文章目录
!!!!使用的JS语言
1. 链表的基础知识
1.1 定义
单链表由一系列节点组成,每个节点包含两个部分:
数据域:存储实际的数据。
指针域:存储指向下一个节点的指针。
下图就是单链表的示意图(图源于代码随想录)
1.2 基本操作
插入:可以在链表的开头、结尾或中间插入新节点。
删除:可以删除链表中的任意节点。
查找:从头节点开始遍历,直到找到目标节点。
掌握了链表的这几种基本操作,基本掌握了链表部分的练习。
1.3 优缺点和应用场景
优点:
- 插入和删除操作效率高,时间复杂度为O(1)。
- 空间利用率高,可以充分利用碎片化的内存。
缺点:
- 随机访问效率低,需要从头遍历。
- 额外的内存开销用于存储指针。
应用场景:
需要频繁插入和删除操作的场景,不需要随机访问的场景。
以JS为例,定义一个单链表结点的代码为:
class ListNode {
constructor(data) {
this.data = data;
this.next = null;
}
}
2. 题目
2.1 移除链表元素
题目:
203.移除链表元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
思路:
这个就是一个基础的删除链表的题目。但需要用到在链表题中经常使用到的技巧——设置虚拟头结点。通过虚拟头节点cur遍历链表,如果cur.val == val ,直接cur.next=cur.next.next便完成了跳过。需要注意的是,考虑边界情况(链表为空、要删除的结点不存在,要删除结点为尾结点)。
答案:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
//创建虚拟头
const ret = new ListNode(0, head);
let cur = ret;
//遍历列表 如果cur下一个节点存在 循环继续
while(cur.next) {
//如果与删除的节点值相等,则跳过这个节点(删除它)
if(cur.next.val === val){
cur.next = cur.next.next;
continue;
}
cur = cur.next;
}
// 创建的虚拟头节点,它始终指向整个链表的起始位置
return ret.next;
};
刚开始使用JS写题可以记住一些基本、常见的表示。
- 定义虚拟头结点,要明白虚拟头结点.next指向头结点。注意const和let/var的使用,如果这个变量在后面代码中没有变化,可以使用const。在这使用了const,是因为我们最后需要ret.next找到链表的头结点并返回。
const ret = new ListNode(0, head);
- 遍历链表,通过虚拟头结点完成
while(cur.next) {
if(cur.next.val === val){
cur.next = cur.next.next;
continue;
}
cur = cur.next;
} - 写完题目后,考虑特殊边界情况。可以带入过一遍,发现无论是空链表还是链表中只有一个结点等情况都符合。
2.2 设计链表
题目:
707.设计链表
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
思路:
这道题基本涵盖了链表的操作,可以仔细理解一下。首先,需要使用到虚拟头结点。除此之外,还需要掌握以下五个接口的实现:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
总结起来还是那三个基本操作:查找、删除、插入。
(1)查找 - 创建虚拟头结点
- 通过cur.next不断遍历,直至到指定Index位置结束循环
- 返回结束循环后的cur
这里有些小细节:循环终止的条件是index>0还是index>=0?cur最后指向的位置正好是所查找元素的位置吗?index指的是第几个元素呢还是数组下标位置?
这时候一定要自己画图确定一下。
/**
* @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 -> head
while(index-- >= 0) {
cur = cur.next;
}
return cur;
};
MyLinkedList.prototype.get = function(index) {
if(index < 0 || index >= this._size) return -1;
return this.getNode(index).val;
};
(2)插入
插入可以分为三种情况:在头结点前插入、在尾结点后插入,在中间结点前插入。
前面两种比较简单,但需要对特殊情况进行处理,如链表为空时。
1)在头结点前插入,直接创建一个新节点,next指向头结点。
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function(val) {
const node = new LinkNode(val, this._head)
this._head = node;
this._size++;
// 处理链表原本为空的情况,确保尾指针也被正确设置
if(!this._tail){
this._tail = node;
}
};
细节:如果处理链表原本为空的情况,确保尾指针也被正确设置;插入操作,size+1
2)在尾结点后插入,直接创建一个新节点,next指向null。
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function(val) {
const node = new LinkNode(val, null)
this._size ++;
// 非空节点的时候
if(this._tail){
this._tail.next = node;
this._tail = node;
return ;
}
this._tail = node;
this._head = node;
};
空链表的时候反而简单,需要注意原来尾结点.next需要指向插入的结点。
3)在中间结点前插入,需要利用到查找的操作,找到被插入结点的前一个位置
//获取目标节点的上一个节点
const node = this.getNode(index - 1);
node.next = new LinkNode(val, node.next);
this._size++;
(3)删除下标为index的结点
也是需要考虑边界情况,比如删除的结点为头结点时,为尾结点时,链表为空时,链表只有一个元素时。
/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function(index) {
if(index < 0 || index >=this._size) return ;
if(index === 0){
this._head = this._head.next;
if(index === this._size-1){
this._tail = this._head
}
this._size--;
return;
}
const node = this.getNode(index-1);
node.next = node.next.next;
if(index === this._size-1){
this._tail = node;
}
this._size --;
};
删除元素size-1;删除特殊结点或者删除后只有一个结点的时候需要设置好头尾结点。
答案:
//定义单链表
class LinkNode {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
// 初始化:储存头尾节点._head _tail和节点数量_size
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 -> head
while(index-- >= 0) {
cur = cur.next;
}
return cur;
};
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) {
const node = new LinkNode(val, this._head)
this._head = node;
this._size++;
// 处理链表原本为空的情况,确保尾指针也被正确设置
if(!this._tail){
this._tail = node;
}
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function(val) {
const node = new LinkNode(val, null)
this._size ++;
// 非空节点的时候
if(this._tail){
this._tail.next = node;
this._tail = node;
return ;
}
this._tail = node;
this._head = node;
};
/**
* @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;
}
//获取目标节点的上一个节点
const node = this.getNode(index - 1);
node.next = new LinkNode(val, node.next);
this._size++;
};
/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function(index) {
if(index < 0 || index >=this._size) return ;
if(index === 0){
this._head = this._head.next;
if(index === this._size-1){
this._tail = this._head
}
this._size--;
return;
}
const node = this.getNode(index-1);
node.next = node.next.next;
if(index === this._size-1){
this._tail = node;
}
this._size --;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* var obj = new MyLinkedList()
* var param_1 = obj.get(index)
* obj.addAtHead(val)
* obj.addAtTail(val)
* obj.addAtIndex(index,val)
* obj.deleteAtIndex(index)
*/
2.3 反转链表
题目:
206.反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路:
这里只介绍双指针的思路,看代码随想录给的动图便很好理解。设置两个相邻的指针,遍历链表的过程中改变指向。
我们需要考虑到两个细节:遍历终止的条件是当cur指向null,需要中间变量保存cur.next的值,因为当cur移动的时候cur.next也随之发生变化(如果先赋值给Pre,那cur无法按照原链表遍历)。
答案:
版本一:双指针
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
// 使用两个指针
var reverseList = function(head) {
let pre = null;
let temp = null;
let cur = head;
//特殊情况 链表为空或者只有一个节点的时候无需反转
if (cur === null || cur.next === null) return head;
while (cur){
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre
};
版本二:递归
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
// 递归 不好理解
var reverse = function(head){
if (head===null || head.next===null) return head;
//递归到最后 返回最后一个结点
const pre = reverse(head.next)
head.next = pre.next;
pre.next = head
return head
}
var reverseList = function(head) {
let cur = head;
//循环结束时cur指向尾结点
while (cur && cur.next){
cur = cur.next;
}
reverse(head);
return cur;
};
2.4 两两交换链表中的节点
题目:
思路:
刚开始做这道题目的时候,理解错了这道理的意思。大家做题之前仔细读题很关键!!
掌握下面这个步骤(虚拟头结点再次出现,可见重要性),我自己也是画图跟着理解的。步骤一一定要在最开始,步骤二和步骤三可以调换位置。因为
答案:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var swapPairs = function(head) {
//考虑链表为空或者只有单节点的时候
if(head === null || head.next ===0) return head;
let ret = new ListNode(0, head);
let temp = ret;
while(temp.next && temp.next.next ){
// 交换分成三步骤 第二步和第三步位置交换不影响
let pre = temp.next;
let cur = temp.next.next;
pre.next = cur.next;
temp.next = cur;
cur.next = pre;
temp = pre;
}
return ret.next;
};
2.5 删除链表的倒数第N个节点
题目:
19. 删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
思路:
- 首先计算链表的长度,while(n-index)正好是要删除的前一个结点(关键)
- 考虑特殊情况,比如n-index刚好等于0的情况。我提交的时候发现通过了一大半用例,后面才发现忽略这一种情况
答案:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function(head, n) {
let p = head;
let length=0;
while( p!==null ){
length++;
p = p.next;
}
let cur = new ListNode(0,head);
let c = length-n;
if(cur.next.next){
if(c===0){return head.next}
while(c){
cur = cur.next;
c--;
}
cur.next = cur.next.next;
return head;
}else{
return null
}
};