Day3-链表,leetcode203,707,206

链表


理论

链表类型

  • 链表,通过指针串联在一起的线性结构。分为单链表、双链表、循环链表。

  • 单链表:每一个节点包含2部分,左、右,左部分是内容区,右部分是指针,指针用于指向下一个节点,最后一个节点的指针指向null。

    • 遍历终止条件:遇到null停止while(current !== null)
  • 双链表:每一个节点包含3个部分,左、中、右,左部分指向上一个节点,中部分是内容区,右部分指向下一个节点。首节点的左指针指向null,尾节点的右指针指向null。

    • 正向遍历,终止条件:current.next === nul
    • 反向遍历,终止条件:current.prev === null
  • 循环链表:链表首尾相连,是一个环形结构。存在循环单链表、循环双链表。尾节点指针指向头节点。

    • 循环单链表,遍历终止条件:回到起点时停止while(current.next !== head)
    • 循环双链表,遍历终止条件:(1)正向遍历,current.next === head;(2)反向遍历,current.prev === head

链表存储方式

  • 数组是在内存中连续分布的,而链表在内存中可不是连续分布的,链表通过指针指向内存中的各个节点

链表定义方式

// 使用类ES6
class ListNode {
    constructor(val, next = null) {
        this.val = val;
        this.next = next;
    }
}
const node11 = new ListNode(1)
const node22 = new ListNode(2, node11)

性能分析

  1. 数组,插入/删除时间复杂度O(n),查询时间复杂度O(1),适用于数据量固定,频繁查询,较少增删;
  2. 链表,插入/删除时间复杂度O(1),查询时间复杂度O(n),适用于数据量不固定,频繁增删,较少查询;

链表的操作

  1. 删除节点
  • 修改被删除节点的前一个节点的next为被删除节点的next
  1. 添加节点
  • 添加的新节点会添加到链表的尾部
  • 链表为空,则新节点设置为头节点;链表不空,遍历到最后一个节点,即current.next为null到节点,然后把新节点挂到最后一个节点的next上
// ListNode类:同样定义了链表节点的结构,包含 val 和 next 属性。
class ListNode {
    constructor(val = 0, next = null) {
        // 定义链表节点类
        this.val = val;
        this.next = next;
    }
}

// LinkedList类:addNode 方法向链表添加新节点,deleteNode 方法用于删除链表中指定值的节点。
class LinkedList {
    constructor() {
        // 初始化链表,设置头节点
        this.head = null
    }
    addNode(val) {
        const newNode = new ListNode(val)
        if (!this.head) {
            this.head = newNode;
            return;
        }
        let current = this.head
        while (current.next) {
            current = current.next
        }
        current.next = newNode
    }
    deleteNode(val) {
        if (!this.head) {
            return
        }
        if (this.head.val === val) {
            this.head = this.head.next
            return
        }
        let current = this.head
        while (current.next) {
            if (current.next.val === val) {
                current.next = current.next.next
                return
            }
            current = current.next
        }
    }
}
// 测试代码
const linkedList = new LinkedList();
linkedList.addNode(1);
linkedList.addNode(2);
linkedList.addNode(3);
console.log('linkedList: ', linkedList);

console.log("删除节点前:");
let current = linkedList.head;
while (current) {
    console.log(current.val);
    current = current.next;
}

linkedList.deleteNode(2);

console.log("删除节点后:");
current = linkedList.head;
while (current) {
    console.log(current.val);
    current = current.next;
}


  1. 移除链表元素
  • 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

  • 思路:

  • 删除时要分是否是头节点,删除的是头节点,则将头节点设置为头节点的next,遍历,while(head!=null && head.val === target) head=head.next;删除的不是头节点的话,直接将当前节点的next设置为next.next,current.next = current.next.next

  • 为了统计思路,可以在头节点前面增加一个虚拟头节点

  • 设置当前值从头节点开始,因为单链表,不能往前找,只能往后找,所以要设置初始节点为当前节点;

  • 返回的是虚拟头节点的next,而不是head,因为虚拟头节点一直存在,而head可能被删除了;

// 区分是否是头节点
var removeElements = function(head, val) {
    // 1. 先处理头节点连续等于 val 的情况
    while (head != null && head.val === val) {
        head = head.next
    }
    // 2. 再处理非头节点
    let current = head;
    while (current !== null && current.next !== null) {
        if (current.next.val === val) {
            current.next = current.next.next
        } else {
            current = current.next
        }
    }
    return head
};

// 统一思路,头节点前面增加一个节点
var removeElements = function(head, val) {
    // new ListNode(0, head) 的意思是:创建一个值为 0、next 指向 head 的链表节点。
    // 0 是节点的值(val),head 是原链表的头节点,作为新节点的 next 指针。
    // 这样做的目的是引入一个虚拟头节点,方便统一处理链表头节点被删除的情况,避免特殊判断。
    const ret = new ListNode(0, head)
    // cur 指针从虚拟头节点开始遍历
    let cur = ret;
     // 遍历链表
    while(cur.next) {
        // 如果下一个节点的值等于 val,则跳过该节点
        if(cur.next.val === val) {
            cur.next = cur.next.next
            // 跳过后不移动 cur,继续检查新的 cur.next
            continue
        }
        // 否则,cur 指针后移
        cur = cur.next
    }
    // 返回新链表的头节点(去掉虚拟头节点)
    return ret.next
}


  1. 设计链表
  • 你可以选择使用单链表或者双链表,设计并实现自己的链表。

  • 单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

  • 如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

  • 实现 MyLinkedList 类:

    • MyLinkedList() 初始化 MyLinkedList 对象。
    • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
    • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
    • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
    • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
    • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。
  • 思路:

  • 初始化链表,定义虚拟头节点

  • get(index),获取到第index节点,如果index非数值直接返回-1,index是从0开始的,第0个节点就是头节点

  • addAtHead,在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头节点

  • addAtTail,在链表最后面添加一个节点

  • addAtIndex,在第index节点之前插入一个新节点,如index为0,则新插入的节点为链表的新头节点,若index等于链表的长度,则新插入的节点为链表的尾节点,若index大于链表的长度,则返回空,若index小于0,则在头部插入节点

  • deleteAtIndex,删除第index个节点,若index大于等于链表的长度,直接return,注意index是从0开始的

// 单链表
class LinkNode {
    constructor(val, next) {
        this.val = val;
        this.next = next;
    }
}
// 单链表,储存头尾节点和节点数量
var MyLinkedList = function() {
    this._size = 0;
    this._tail = null;
    this._head = null;
}

MyLinkedList.prototype.getNode = function(index) {
    if (index < 0 || index >= this._size) return null
    // 创建虚拟头节点
    let cur = new LinkNode(0, this._head)
    while(index-- >= 0) {
        cur = cur.next
    }
    return cur
}
MyLinkList.prototype.get = function(index) {
    if (index < 0 || index >= this._size) return -1
    // 获取当前节点
    return this.getNode(index).val
}

MyLinkedList.prototype.addAtHead = function(val) {
    const node = new LinkNode(val, this._head)
    this._head = node
    this._size++
    if(!this._tail) {
        this._tail = node
    }
}

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
}

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++
}

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--
}
// 双链表,包含前指针prev,后指针next
class Node {
    constructor(val, prev, next) {
        this.val = val;
        this.prev = prev;
        this.next = next;
    }
}

// 双链表,维护head、tail两个哨兵,size
var MyLinkedList = function() {
    this.tail = new Node(-1)
    this.head = new Node(-1)
    this.tail.prev = this.head
    this.head.next = this.tail
    this.size = 0
}

// 获取在index处节点的值
MyLinkedList.prototype.get = function (index) {
    if (index > this.size) {
        return -1
    }
    let cur = this.head
    for (let i = 0; i <= index; i++) {
        cur = cur.next
    }
    return cur.val
}

// 在链表头部添加一个新节点
MyLinkedList.prototype.addAtHead = function(val) {
    this.size++
    const originNode = this.head.next
    // 创建新节点,并建立连接
    const newNode = new Node(val, this.head, originNode)
    // 取消原前后节点的连接
    this.head.next = newNode
    originNode.prev = newNode
}

// 在链表尾部添加一个新节点
MyLinkedList.prototype.addAtTail = function (val) {
    /** 
        originNode <-> [newNode] <-> tail
    */
    this.size++
    const originNode = this.tail.prev

    // 创建新节点,并建立连接
    const newNode = new Node(val, originNode, this.tail)

    // 取消原前后结点的连接
    this.tail.prev = newNode
    originNode.next = newNode
};

// 在指定索引位置前添加一个新节点
MyLinkedList.prototype.addAtIndex = function (index, val) {
    // 当索引超出范围时,直接返回
    if (index > this.size) {
        return
    }
    this.size++

    let cur = this.head
    for (let i = 0; i < index; i++) {
        cur = cur.next
    }

    const new_next = cur.next

    // 创建新节点,并建立连接
    const node = new Node(val, cur, new_next)

    // 取消原前后结点的连接
    cur.next = node
    new_next.prev = node
};

// 删除指定索引位置的节点
MyLinkedList.prototype.deleteAtIndex = function (index) {
    // 当索引超出范围时,直接返回
    if (index >= this.size) {
        return
    }

    this.size--
    let cur = this.head
    for (let i = 0; i < index; i++) {
        cur = cur.next
    }

    const new_next = cur.next.next
    // 取消原前后结点的连接
    new_next.prev = cur
    cur.next = new_next
};



  1. 反转链表
  • 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
// 双指针
var reverseList = function(head) {
    if (!head || !head.next) return head
    let temp = null, pre = null, cur = head
    while(cur) {
        // 记录curr下一节点的位置
        temp = cur.next
        // 反转节点间指针的方向
        cur.next = pre
        // 将prev指针向后移动一位
        pre = cur
        // 将curr指针向后移动一位
        cur = temp
    }
    return pre
}

// 递归:
var reverse = function(pre, head) {
    if(!head) return pre;
    const temp = head.next;
    head.next = pre;
    pre = head
    return reverse(pre, temp);
}

var reverseList = function(head) {
    return reverse(null, head);
};

// 递归2
var reverse = function(head) {
    if(!head || !head.next) return head;
    // 从后往前翻
    const pre = reverse(head.next);
    head.next = pre.next;
    pre.next = head;
    return head;
}

var reverseList = function(head) {
    let cur = head;
    while(cur && cur.next) {
        cur = cur.next;
    }
    reverse(head);
    return cur;
};



参考&感谢各路大神

posted @ 2025-05-31 06:31  安静的嘶吼  阅读(4)  评论(0)    收藏  举报