Skip to content

链表结构(LinkedList)

链表以及数组的缺点

链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同

这一章中,我们就来学习一下另外一种非常常见的用于存储数据的线性结构:链表

数组

  • 要存储多个元素,数组(或选择链表)可能是最常用的数据结构。
  • 我们之前说过,几乎每一种编程语言都有默认实现数组结构

但是数组也有很多缺点:

  • 数组的创建通常需要申请一段连续的内存空间(一整块的内存),并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容。(一般情况下是申请一个更大的数组,比如 2 倍。然后将原数组中的元素复制过去)
  • 而且在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移
  • 尽管 JavaScript 的 Array 底层可以帮我们做这些事,但背后的原理依然是这样。

链表的优势

要存储多个元素,另外一个选择就是链表

但不同于数组,链表中的元素在内存中不必是连续的空间

  • 链表的每个元素由一个存储元素本身的节点指向下一个元素的引用(有些语言称为指针或者链接)组成。

相对于数组,链表有一些优点:

  • 内存空间不是必须连续的
    • 可以充分利用计算机的内存,实现灵活的内存动态管理
  • 链表不必在创建时就确定大小,并且大小可以无限的延伸下去。
  • 链表在插入和删除数据时,时间复杂度可以达到 O(1)。
    • 相对数组效率高很多

相对于数组,链表有一些缺点:

  • 链表访问任何一个位置的元素时,都需要从头开始访问。(无法跳过第一个元素访问任何一个元素)。
  • 无法通过下标直接访问元素,需要从头一个个访问。直到找到对应的元素。

链表到底是什么?

什么是链表呢?

  • 其实上面我们已经简单的提过了链表的结构,我们这里更加详细的分析一下。
  • 链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推。

链表的火车结构:

链表结构的封装

我们先来创建一个链表类

代码解析:

  • 封装一个Node 类,用于封装每一个节点上的信息(包括值和指向下一个节点的引用),它是一个泛型类。
  • 封装一个LinkedList类,用于表示我们的链表结构。(和 Java 中的链表同名,不同 Java 中的这个类是一个双向链表,在第二阶段中我们也会实现双向链表结构)。
  • 链表中我们保存两个属性,一个是链表的长度,一个是链表中第一个节点
  • 当然,还有很多链表的操作方法。我们放在下一节中学习。

01_实现 LinkedList(封装).ts

typescript
// 1.创建Node 节点类
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;

  get length() {
    return this.size;
  }
}

const linkedList = new LinkedList<string>();
console.log(linkedList.head);

export {};

链表常见操作

我们先来认识一下,链表中应该有哪些常见的操作

  • append(element):向链表尾部添加一个新的项
  • insert(position, element):向链表的特定位置插入一个新的项
  • get(position):获取对应位置的元素
  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1
  • update(position, element):修改某个位置的元素
  • removeAt(position):从链表的特定位置移除一项
  • remove(element):从链表中移除一项
  • isEmpty():如果链表中不包含任何元素,返回 true,如果链表长度大于 0 则返回 false
  • size():返回链表包含的元素个数。与数组的 length 属性类似。

整体你会发现操作方法和数组非常类似,因为链表本身就是一种可以代替数组的结构。

append 方法

向链表尾部追加数据可能有两种情况:

  • 链表本身为空,新添加的数据是唯一的节点。
  • 链表不为空,需要向其他节点后面追加节点。

链表的遍历方法(traverse)

为了可以方便的看到链表上的每一个元素,我们实现一个遍历链表每一个元素的方法:

  • 这个方法首先将当前节点设置为链表的头结点。
  • 然后,在 while 循环中,我们遍历链表并打印当前节点的数据。
  • 在每次迭代中,我们将当前节点设置为其下一个节点,直到遍历完整个链表。

02_实现 LinkedList(添加).ts

typescript
// 1.创建Node 节点类
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;

  get length() {
    return this.size;
  }

  // 追加结点
  append(value: T) {
    // 1.根据value创建一个新节点
    const newNode = new Node(value);

    // 2.判断this.head是否为null
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      // current肯定是指向最后一个结点的
      current.next = newNode;
    }

    // 3.size++
    this.size++;
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = [];
    let current = this.head;
    while (current) {
      values.push(current.value);
      current = current.next;
    }
    console.log(values.join("->"));
  }
}

insert方法

接下来实现另外一个添加数据的方法:在任意位置插入数据

添加到第一个位置

  • 添加到第一个位置,表示新添加的节点是头,就需要将原来的头节点,作为新节点的next
  • 另外这个时候的head应该指向新节点。

添加到其他位置

  • 如果是添加到其他位置,就需要先找到这个节点位置了。
  • 我们通过while循环,一点点向下找。 并且在这个过程中保存上一个节点和下一个节点。
  • 找到正确的位置后,将新节点的next指向下一个节点,将上一个节点的next指向新的节点。

03_实现 LinkedList(插入).ts

TypeScript
// 1.创建Node 节点类
class Node<T> {
  value: T
  next: Node<T> | null = null
  constructor(value: T){
    this.value = value
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null
  private size: number = 0

  get length() {
    return this.size
  }

  // 追加结点
  append(value: T) {
    // 1.根据value创建一个新节点
    const newNode = new Node(value)

    // 2.判断this.head是否为null
    if(!this.head) {
      this.head = newNode
    }else {
      let current = this.head
      while(current.next) {
        current = current.next
      }
      // current肯定是指向最后一个结点的
      current.next = newNode
    }

    // 3.size++
    this.size++
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = []
    let current = this.head
    while(current) {
      values.push(current.value)
      current = current.next
    }
    console.log(values.join("->"))
  }

  // 插入方法
  insert(value: T, position: number) {
    // 1.越界判断
  	if(position < 0 || positon > this.size) return false

    // 2.根据value创建新的节点
    const newNode = new Node(value)

    // 3.判断是否需要插入头部
    if(position === 0) {
      newNode.next = this.head
      this.head = newNode
    } else {
      let current = this.head
      let previous: Node<T> | null = null
      let index = 0
      while (index++ < positon && current) {
        previous = current
        current = current.next
      }
      newNode.next = current
      previous!.next = newNode
    }
    this.size++
    return true
  }
}

removeAt 方法

移除数据有两种常见的方式:

  • 根据位置移除对应的数据
  • 根据数据,先找到对应的位置,再移除数据

移除第一项的信息:

  • 移除第一项时,直接让 head 指向第二项信息就可以了。
  • 那么第一项信息没有引用指向,就在链表中不再有效,后面会被回收掉

移除其他项的信息:

  • 移除其他项的信息操作方式是相同的。
  • 首先,我们需要通过 while 循环,找到正确的位置。
  • 找到正确位置后,就可以直接将上一项的 next 指向 current 项的 next,这样中间的项就没有引用指向它,也就不再存在于链表后,后面会被回收掉。

04_实现 LinkedList(删除).ts

typescript
// 1.创建Node 节点类
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;

  get length() {
    return this.size;
  }

  // 追加结点
  append(value: T) {
    // 1.根据value创建一个新结点
    const newNode = new Node(value);

    // 2.判断this.head是否为null
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      // current肯定是指向最后一个结点的
      current.next = newNode;
    }

    // 3.size++
    this.size++;
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = [];
    let current = this.head;
    while (current) {
      values.push(current.value);
      current = current.next;
    }
    console.log(values.join("->"));
  }

  // 插入方法:abc
  insert(value: T, position: number) {
    // 1.越界判断
    if (position < 0 || position > this.size) return false;

    // 2.根据value创建新的结点
    const newNode = new Node(value);

    // 3.判断是否需要插入头部
    if (position === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else {
      let current = this.head;
      let previous: Node<T> | null = null;
      let index = 0;
      while (index++ < position && current) {
        previous = current;
        current = current.next;
      }
      newNode.next = current;
      previous!.next = newNode;
    }
    this.size++;
    return true;
  }

  // 删除方法:
  removeAt(position: number): T | null {
    // 1.越界判断
    if (positon < 0 || position >= this.size) return null;

    // 2.判断是否是删除第一个节点
    let current = this.head;
    if (positon === 0) {
      this.head = current?.next ?? null;
    } else {
      let previous: Node<T> | null = null;
      let index = 0;
      while (index++ < positon && current) {
        previous = current;
        current = current.next;
      }

      // 找到需要的节点
      previous!.next = current.next ?? null;
    }

    this.size--;
    return current?.value ?? null;
  }
}
export {};

get 方法

get(position):获取对应位置的元素

05_实现 LinkedList(获取).ts

typescript
// 1.创建Node 节点类
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;

  get length() {
    return this.size;
  }

  // 追加结点
  append(value: T) {
    // 1.根据value创建一个新结点
    const newNode = new Node(value);

    // 2.判断this.head是否为null
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      // current肯定是指向最后一个结点的
      current.next = newNode;
    }

    // 3.size++
    this.size++;
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = [];
    let current = this.head;
    while (current) {
      values.push(current.value);
      current = current.next;
    }
    console.log(values.join("->"));
  }

  // 插入方法:abc
  insert(value: T, position: number) {
    // 1.越界判断
    if (position < 0 || position > this.size) return false;

    // 2.根据value创建新的结点
    const newNode = new Node(value);

    // 3.判断是否需要插入头部
    if (position === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else {
      let current = this.head;
      let previous: Node<T> | null = null;
      let index = 0;
      while (index++ < position && current) {
        previous = current;
        current = current.next;
      }
      newNode.next = current;
      previous!.next = newNode;
    }
    this.size++;
    return true;
  }

  // 删除方法:
  removeAt(position: number): T | null {
    // 1.越界判断
    if (position < 0 || position >= this.size) return null;

    // 2.判断是否是删除第一个结点
    let current = this.head;
    if (position === 0) {
      this.head = current?.next ?? null;
    } else {
      let previous: Node<T> | null = null;
      let index = 0;
      while (index++ < position && current) {
        previous = current;
        current = current.next;
      }

      // 找到需要的结点
      previous!.next = current.next ?? null;
    }

    this.size--;
    return current?.value ?? null;
  }

  // 获取方法
  get(position: number): T | null {
    // 1.越界问题
    if (positon < 0 || positon >= this.size) return null;

    // 2.查找元素,并且返回元素
    let index = 0;
    let current = this.head;
    while (index++ < positon && current) {
      current = current.next;
    }

    // index === positon
    return current?.value ?? null;
  }
}

export {};

遍历节点的操作重构

因为遍历节点的操作我们需要经常来做,所以可以进行如下的重构:

其他需要获取节点的,可以调用上面的方法,比如操作操作:

06_实现LinkedList(重构).ts

typescript
// 1.创建Node 节点类
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;

  get length() {
    return this.size;
  }

  // 封装私有方法
  // 根据position获取到当前的节点(不是节点的value,而是获取节点)
  private getNode(position: number): Node<T> | null {
    let index = 0;
    let current = this.head;
    while (index++ < positon && current) {
      current = current.next;
    }
    return current;
  }

  // 追加节点
  append(value: T) {
    // 1.根据value创建一个新结点
    const newNode = new Node(value);

    // 2.判断this.head是否为null
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      // current肯定是指向最后一个结点的
      current.next = newNode;
    }

    // 3.size++
    this.size++;
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = [];
    let current = this.head;
    while (current) {
      values.push(current.value);
      current = current.next;
    }
    console.log(values.join("->"));
  }

  // 插入方法
  insert(value: T, position: number) {
    // 1.越界判断
    if (positon < 0 || position > this.size) return false;

    // 2.根据value创建新的节点
    const newNode = new Node(value);

    // 3.判断是否需要插入头部
    if (position === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else {
      const previous = this.getNode(positon - 1);
      newNode.next = previous!.next;
      previous!.next = newNode;
    }
    this.size++;
    return true;
  }

  // 删除方法
  removeAt(position: number): T | null {
    // 1.越界判断
    if (position < 0 || position >= this.size) return null;

    // 2.判断是否是删除第一个节点
    let current = this.head;
    if (position === 0) {
      this.head = current?.next ?? null;
    } else {
      const previous = this.getNode(positon - 1);
      previous!.next = previous?.next.next ?? null;
    }

    this.size--;
    return current?.value ?? null;
  }

  // 获取方法
  get(position: number): T | null {
    // 1.越界问题
    if (position < 0 || position >= this.size) return null;

    // 2.查找元素,并且返回元素
    return this.getNode(position)?.value ?? null;
  }
}

export {};

update 方法、remove 方法以及其他方法

update(position, element): 修改某个位置的元素

indexOf 方法

我们来完成另一个功能:根据元素获取它在链表中的位置

有了上面的 indexOf 方法,我们可以非常方便实现根据元素来删除信息

isEmpty 方法

07_实现 LinkedList(更新).ts

typescript
// 1.创建Node 节点类
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

// 2.创建linkedList的类
class LinkedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;

  get length() {
    return this.size;
  }

  // 封装私有方法
  // 根据position获取到当前的结点(不是结点的value,而是获取结点)
  private getNode(position: number): Node<T> | null {
    let index = 0;
    let current = this.head;
    while (index++ < position && current) {
      current = current.next;
    }
    return current;
  }

  // 追加结点
  append(value: T) {
    // 1.根据value创建一个新结点
    const newNode = new Node(value);

    // 2.判断this.head是否为null
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      // current肯定是指向最后一个结点的
      current.next = newNode;
    }

    // 3.size++
    this.size++;
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = [];
    let current = this.head;
    while (current) {
      values.push(current.value);
      current = current.next;
    }
    console.log(values.join("->"));
  }

  // 插入方法:abc
  insert(value: T, position: number) {
    // 1.越界判断
    if (position < 0 || position > this.size) return false;

    // 2.根据value创建新的结点
    const newNode = new Node(value);

    // 3.判断是否需要插入头部
    if (position === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else {
      const previous = this.getNode(position - 1);
      newNode.next = previous!.next;
      previous!.next = newNode;
    }
    this.size++;
    return true;
  }

  // 删除方法:
  removeAt(position: number): T | null {
    // 1.越界判断
    if (position < 0 || position >= this.size) return null;

    // 2.判断是否是删除第一个结点
    let current = this.head;
    if (position === 0) {
      this.head = current?.next ?? null;
    } else {
      // 重构成如下代码
      const previous = this.getNode(position - 1);

      // 找到需要的结点
      previous!.next = previous?.next.next ?? null;
    }

    this.size--;
    return current?.value ?? null;
  }

  // 获取方法
  get(position: number): T | null {
    // 1.越界问题
    if (position < 0 || position >= this.size) return null;

    // 2.查找元素,并且返回元素
    return this.getNode(positon)?.value ?? null;
  }

  // 更新方法
  update(value: T, position: number): boolean {
    if (position < 0 || position >= this.size) return false;
    // 获取对应位置的节点,直接更新即可
    const currentNode = this.getNode(position);
    currentNode!.value = value;
    return true;
  }

  // 根据值,获取对应位置的索引
  indexOf(value: T): Number {
    // 从第一个结点开始,向后遍历
    let current = this.head;
    let index = 0;
    while (current) {
      if (current.value === value) {
        return index;
      }
      current = current.next;
      index++;
    }
    return -1;
  }

  // 删除方法:根据value删除节点
  remove(value: T): T | null {
    const index = this.indexOf(value);
    return this.removeAt(index);
  }

  // 判断单链表是否为空的方法
  isEmpty() {
    return this.size === 0;
  }
}

const linkedList = new LinkedList<string>();
export {};

707.设计链表 - 字节、腾讯等公司面试题

707.设计链表

设计链表的实现。

  • 您可以选择使用单链表或双链表。
  • 单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。
  • 如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtIndex(index, val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果 index 小于 0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

237.删除链表中的节点 - 字节、阿里等公司面试题

237.删除链表中的节点

有一个单链表的 head,我们想删除它其中的一个节点 node。

  • 给你一个需要删除的节点 node。
  • 你将 无法访问 第一个节点 head。

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:

  • 给定节点的值不应该存在于链表中。
  • 链表中的节点数应该减少 1。
  • node 前面的所有值顺序相同。
  • node 后面的所有值顺序相同。

10_面试题二-删除 Node 结点.ts

typescript
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val?: number, next?: ListNode | null) {
    this.val = val === undefined ? 0 : val;
    this.next = next === undefined ? null : next;
  }
}

function deleteNode(node: ListNode | null): void {
  node!.val = node!.next!.val;
  node!.next = node.next!.next;
}

206.反转链表 - 字节、谷歌等面试题

206. 反转链表

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

面试题_ListNode.ts

typescript
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val?: number, next?: ListNode | null) {
    this.val = val === undefined ? 0 : val;
    this.next = next === undefined ? null : next;
  }
}

export default ListNode;
typescript
import ListNode from "./面试题_ListNode";

function reverseList(head: ListNode | null): ListNode | null {
  // 什么情况下链表不需要处理?
  // 1.head 本身为null的情况
  if (head === null) return null;
  // 2.只有head一个节点
  if (head.next === null) return head;

  // 数组模拟栈结构
  const stack: ListNode[] = [];
  let current: ListNode | null = head;
  while (current) {
    stack.push(current);
    current = current.next;
  }

  // 依次从栈结构中取出元素,放到一个新的链表中
  const newHead: ListNode = stack.pop();
  const newHeadCurrent = newHead;
  while (stack.length) {
    const node = stack.pop();
    newHeadCurrent!.next = node;
    newHeadCurrent = newHeadCurrent.next;
  }
  // 注意:获取到最后一个节点时,一定要将节点的next置为null
  newHeadCurrent.next = null;

  return newHead;
}

循环方式(非递归)

  1. 让 current 节点指向下一个节点

      目的:保留着下一个节点的引用,可以拿到,并且不会销毁

  1. 改变 head 当前指向的节点,指向 newHead

      对于第一个节点来说,指向 newHead 就是指向的 null

  1. 让 newHead 指向 head 节点

      目的是下一次遍历时,第二步操作,可以让下一个节点指向第一个节点

  1. 让 head 移向下一个节点,指向 current

13_面试题三·反转链表(非递归).ts

typescript
import ListNode from "./面试题_ListNode";

function reverseList(head: ListNode | null): ListNode | null {
  // 1.判断节点为null,或者只要有一个节点,那么直接返回即可
  if (head === null || head.next === null) return head;

  // 2.反转链表结构
  let newHead: ListNode | null = null;

  while (head) {
    const current: ListNode | null = head.next;
    head.next = newHead;
    newHead = head;
    head = current;
  }

  return newHead;
}

14_面试题三·反转链表(递归).ts

typescript
import ListNode from "./面试题_ListNode";

function reverseList(head: ListNode | null): ListNode | null {
  // 如果使用的是递归,那么递归必须有结束条件
  if (head === null || head.next === null) return head;
  const newHead = reverseList(head?.next ?? null);

  // 完成想要做的操作是在这个位置
  // 第一次来到这里的时候,是倒数第二个节点
  head.next.next = head;
  head.next = null;

  return newHead;
}

定义链表接口 ILinkedList.ts

typescript
import IList from "../types/IList";

interface ILinkedList<T> extends IList<T> {
  append(value: T): void;
  traverse(): void;
  insert(value: T, position: number): boolean;
  removeAt(position: number): T | null;
  get(position: number): T | null;
  update(value: T, position: number): boolean;
  indexOf(value: T): number;
  remove(value: T): T | null;
}

export default ILinkedList;

15_实现 LinkedList(接口设计).ts

typescript
import ILinkedList from './ILinkedList'

// 1.创建Node 节点类
class Node<T> {
  value: T
  next: Node<T> | null = null
  constructor(value: T) {
    this.value = value
  }
}

// 2.创建linkedList的类
class LinkedList<T> implements ILinkedList<T> {
  head: Node<T> | null = null
  private length: number = 0

  size() {
    return this.length
  }

  peek(): T | undefined {
    return this.head?.value
  }

  // 封装私有方法
  // 根据position获取到当前的节点(不是节点的value,而是获取节点)
  privare getNode(position: number): Node<T> | null {
    let index = 0
    let current = this.head
    while(index++ < position && current) {
      current = current.next
    }
    return current
  }

  // 追加节点
  append(value:T) {
    // 1.根据value创建一个新节点
    const newNode = new Node(value)

    // 2.判断this.head是否为null
    if(!this.head) {
      this.head = newNode
    }else {
      let current = this.head
      while(current.next) {
        current = current.next
      }
      // current肯定是指向最后一个节点的
      current.next = newNode
    }

    // 3.length++
    this.length++
  }

  // 遍历链表的方法
  traverse() {
    const values: T[] = []
    let current = this.head
    while(current) {
      values.push(current.value)
      current = current.next
    }
    console.log(values.join("->"))
  }

  // 插入方法
  insert(value: T, position: number) {
    // 1.越界判断
    if(position < 0 || position > this.length) return false

    // 2.根据value创建新的节点
    const newNode = new Node(value)

    // 3.判断是否需要插入头部
    if(position === 0) {
      newNode.next = this.head
      this.head = newNode
    } else {
      const previous = this.getNode(position - 1)
      newNode.next = previous!.next
      previous!.next = newNode
    }
    this.length++
    return true
  }

  // 删除方法
  removeAt(position: number): T | null {
    // 1.越界判断
    if(position < 0 || position >= this.head.length) return null

    // 2.判断是否是删除第一个节点
    let current = this.head
    if(position === 0) {
      this.head = current?.next ?? null
    } else {
      const previous = this.getNode(position - 1)

      // 找到需要的节点
      previous!.next = previous?.next.next ?? null
    }

    this.length--
    return current
  }

  // 获取方法
  get(position: number): T | null {
    // 1.越界问题
    if (position < 0 || position >= this.length) return null

    // 2.查找元素,并且返回元素
    return this.getNode(position)?.value ?? null
  }

  // 更新方法
  update(value: T, position: number): boolean {
    if(position < 0 || position >= this.length) return false
    // 获取对应位置的节点,直接更新即可
    const currentNode = this.getNode(position)
    currentNode!.value = value
    return true
  }

  // 根据值,获取对应位置的索引
  indexOf(value: T): number {
    // 从第一个节点开始,向后遍历
    let current = this.head
    let index = 0
    while(current) {
      if (current.value === value) {
        return index
      }
      current = current.next
      index++
    }
    return -1
  }

  // 删除方法:根据value删除节点
  remove(value: T): T | null {
    const index = this.indexOf(value)
    return this.removeAt(index)
  }

  // 判断单链表是否为空的方法
  isEmpty() {
    return this.length === 0
  }
}

什么是算法复杂度(现实案例)?

前面我们已经解释了什么是算法?其实就是解决问题的一系列步骤操作、逻辑

对于同一个问题,我们往往其实有多种解决它的思路和方法,也就是可以采用不同的算法

  • 但是不同的算法,其实效率是不一样

举个例子(现实的例子):在一个庞大的图书馆中,我们需要找一本书。

  • 在图书已经按照某种方式摆好的情况下(数据结构是固定的)

方式一:顺序查找

  • 一本本找,直到找到想要的书:(累死)

方式二:先找分类,分类中找这本书

  • 先找到分类,在分类中再顺序或者某种方式查找

方式三:找到一台电脑,查找书的位置,直接找到;

  • 图书馆通常有自己的图书管理系统
  • 利用图书管理系统先找到书的位置,再直接过去找到

什么是算法复杂度(程序案例)?

我们再举一个程序中的案例:让我们来比较两种不同算法在查找数组中(数组有序)给定元素的时间复杂度。

方式一:顺序查找

  • 这种算法从头到尾遍历整个数组,依次比较每个元素给定元素的值
  • 如果找到相等的元素,则返回下标;如果遍历完整个数组都没找到,则返回-1

方式二:二分查找

  • 这种算法假设数组是有序的,每次选择数组中间的元素与给定元素进行比较
  • 如果相等,则返回下标;如果给定元素比中间元素小,则在数组的左半部分继续查找
  • 如果给定元素比中间元素大,则在数组的右半部分继续查找
  • 这样每次查找都会将查找范围减半,直到找到相等的元素或者查找范围为空

01_查找算法-顺序查找.ts

typescript
/**
 * 顺序查找的算法
 * @param array 查找的数组
 * @param num 查找的元素
 * @returns 查找到的索引,未找到返回-1
 */

function sequentSearch(array: number[], num: number) {
  const length = array.length;
  for (let i = 0; i < length; i++) {
    const item = array[i];
    if (item === num) {
      return i;
    }
  }
  return -1;
}

const index = sequentSearch([1, 3, 5, 10, 100, 222, 333], 222);

export default sequentSearch;

02_查找算法-二分查找.ts

typescript
function binarySearch(array: number[], num: number) {
  // 1.定义左边的索引
  let left = 0;
  // 2.定义右边的索引
  let right = array.length - 1;

  // 3.开始查找
  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    const midNum = array[mid];
    if (midNum === num) {
      return midNum;
    } else if (minNum < num) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return -1;
}

顺序查找和二分查找的测试代码

顺序查找:

二分查找:

03_测试查找算法时间.ts

typescript
import { testOrderSearchEfficiency } from "hy-algokit";

import sequentSearch from "./01_查找算法-顺序查找";
import binarySearch from "./02_查找算法·二分查找";

const MAX_LENGTH = 1000000;
const nums = new Array(1000000).fill(0).map((_, index) => index);
const num = MAX_LENGTH / 2;

const startTime = performance.now(); // new Date().getTime()
// const index = sequentSearch(nums, num)
const index = binarySearch(nums, num);

const endTime = performance.now(); // new Date().getTime()
console.log("索引的位置:", index, "消耗的时间:", endTime - startTime);

testOrderSearchEfficiency(sequentSearch);
testOrderSearchEfficiency(binarySearch);

大 O 表示法(Big O notation)

大 O 表示法(Big O notation)英文翻译为大 O 符号(维基百科翻译),中文通常翻译为大 O 表示法(标记法)。

  • 这个记号则是在德国数论学家爱德蒙·兰道的著作中才推广的,因此它有时又称为兰道符号(Landau symbols)
  • 代表“order of ...(......阶)大 O,最初是一个大写希腊字母“O”(omicron),现今用的是大写拉丁字母“O”

大 O 符号在分析算法效率的时候非常有用

  • 举个例子,解决一个规模为 n 的问题所花费的时间(或者所需步骤的数目)可以表示为:T(n) = 4n² - 2n + 2
    • n 增大时,项开始占据主导地位,其他各项可以被忽略
  • 举例说明:当 n=500
    • 4n²项是2n 项1000 倍大,因此在大多数场合下,省略后者对表达式的值的影响将是可以忽略不计的
    • 进一步看,如果我们与任一其他级的表达式比较,n² 的系数也是无关紧要的

我们就说该算法具有 n² 阶(平方阶)的时间复杂度,表示为 O(n²)

大 O 表示法 - 常见的对数阶

常用的函数阶

空间复杂度

空间复杂度指的是程序运行过程中所需要的额外存储空间

  • 空间复杂度也可以用大 O 表示法来表示
  • 空间复杂度的计算方法与时间复杂度类似,通常需要分析程序中需要额外分配的内存空间,如数组、变量、递归调用等。

举个例子

  • 对于一个简单的递归算法来说,每次调用都会在内存中分配新的栈帧,这些栈帧占用了额外的空间
    • 因此,该算法的空间复杂度是 O(n),其中 n 是递归深度。
  • 而对于迭代算法来说,在每次迭代中不需要分配额外的空间,因此其空间复杂度为 O(1)

当空间复杂度很大时,可能会导致内存不足,程序崩溃。

在平时进行算法优化时,我们通常会进行如下的考虑:

  • 使用尽量少的空间(优化空间复杂度)
  • 使用尽量少的时间(优化时间复杂度)
  • 特定情况下:使用空间换时间或使用时间换空间

数组和链表的复杂度对比

接下来,我们使用大 O 表示法来对比一下数组和链表的时间复杂度:

数组是一种连续的存储结构,通过下标可以直接访问数组中的任意元素。

  • 时间复杂度:对于数组,随机访问时间复杂度为 O(1),插入和删除操作时间复杂度为 O(n)。
  • 空间复杂度:数组需要连续的存储空间,空间复杂度为 O(n)。

链表是一种链式存储结构,通过指针链接起来的节点组成,访问链表中元素需要从头节点开始遍历。

  • 时间复杂度:对于链表,随机访问时间复杂度为 O(n),插入和删除时间复杂度为 O(1)。
  • 空间复杂度:链表需要为每个节点分配存储空间,空间复杂度为 O(n)。

在实际开发中,选择使用数组还是链表需要根据具体应用场景来决定。

  • 如果数据量不大,且需要频繁随机访问元素,使用数组可能会更好。
  • 如果数据量大,或者需要频繁插入和删除元素,使用链表可能会更好。