【代码随想录】算法练习—02单链表

!!!!使用的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 两两交换链表中的节点

题目:

24.两两交换链表中的节点
在这里插入图片描述

思路:

刚开始做这道题目的时候,理解错了这道理的意思。大家做题之前仔细读题很关键!!
掌握下面这个步骤(虚拟头结点再次出现,可见重要性),我自己也是画图跟着理解的。步骤一一定要在最开始,步骤二和步骤三可以调换位置。因为
在这里插入图片描述

答案:

/**
 * 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
}

};

总结

在这里插入图片描述

  • 16
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值