数据结构-链表

40 阅读4分钟

在这篇文章中,我将用代码的方式,给大家呈现出链表在程序里面是什么样子,以及链表有什么方法对外暴露

我们需要做以下准备

  1. typescript开发语言
  2. nodejs环境

链表的定义

  • 特点:通过“指针”将一组零散的内存块串联起来。插入和删除效率高(只需要改变指针指向,时间复杂度 O(1))。但访问元素需要从头遍历(时间复杂度 O(n)),且需要额外的空间存储指针。

  • 常见变种

    • 单向链表:每个节点只指向下一个节点。
    • 双向链表:每个节点指向前一个和后一个节点,支持双向遍历。
    • 循环链表:尾节点指向头节点,形成一个环。
  • 场景:频繁进行插入和删除操作、不确定数据规模的情况(如实现队列、LRU缓存)。

那接下来我们用typescript实现一个双向链表

代码实现

1. 首先我们需要先定义链表里面节点的类型

// 定义链表节点类
class DoublyLinkedListNode<T> {
  value: T;
  next: DoublyLinkedListNode<T> | null;
  prev: DoublyLinkedListNode<T> | null;

  constructor(value: T) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

2. 接下来开始编写我们的链表类 DoublyLinkedList

// 定义双向链表类
class DoublyLinkedList<T> {
  private head: DoublyLinkedListNode<T> | null;
  private tail: DoublyLinkedListNode<T> | null;
  private _size: number;

  constructor() {
    this.head = null;
    this.tail = null;
    this._size = 0;
  }
}

熟悉数据结构的同学都知道,双向链表或单向链表结构里面会有头部和尾部两个元素

3. 接下来我们给我们的链表类添加方法

获取链表长度

  // 获取链表大小
  get size(): number {
    return this._size;
  }

头部添加元素

这个方法还是很简单的,

  1. 如果长度为0

        1.1 将头部和尾部都指向新节点

  1. 如果长度大于0,那么我们看看怎么插入的

  // 在链表头部添加节点
  prepend(value: T): void {
    const newNode = new DoublyLinkedListNode(value);
    
    if (this._size === 0) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head!.prev = newNode;
      this.head = newNode;
    }
    
    this._size++;
  }

尾部添加元素

尾部添加元素和头部添加元素有异曲同工之处

  // 在链表尾部添加节点
  append(value: T): void {
    const newNode = new DoublyLinkedListNode(value);
    
    if (this._size === 0) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail!.next = newNode;
      newNode.prev = this.tail;
      this.tail = newNode;
    }
    
    this._size++;
  }

指定位置添加元素

  // 在指定位置插入节点
  insertAt(index: number, value: T): boolean {
    if (index < 0 || index > this._size) return false;
    
    if (index === 0) {
      this.prepend(value);
      return true;
    }
    
    if (index === this._size) {
      this.append(value);
      return true;
    }
    
    const newNode = new DoublyLinkedListNode(value);
    let current = this.head;
    
    for (let i = 0; i < index - 1; i++) {
      current = current!.next;
    }
    
    newNode.next = current!.next;
    newNode.prev = current;
    current!.next!.prev = newNode;
    current!.next = newNode;
    
    this._size++;
    return true;
  }

删除指定位置的节点

 removeAt(index: number): T | null {
    if (index < 0 || index >= this._size) return null;
    
    let current = this.head;
    
    if (index === 0) {
      this.head = current!.next;
      
      if (this.head) {
        this.head.prev = null;
      } else {
        this.tail = null;
      }
    } else if (index === this._size - 1) {
      current = this.tail;
      this.tail = current!.prev;
      this.tail!.next = null;
    } else {
      for (let i = 0; i < index; i++) {
        current = current!.next;
      }
      
      current!.prev!.next = current!.next;
      current!.next!.prev = current!.prev;
    }
    
    this._size--;
    return current!.value;
  }

查找元素

// 查找节点值
  find(value: T): number {
    let current = this.head;
    let index = 0;
    
    while (current) {
      if (current.value === value) {
        return index;
      }
      
      current = current.next;
      index++;
    }
    
    return -1;
  }

获取指定位置的元素

  // 获取指定位置的节点值
  getAt(index: number): T | null {
    if (index < 0 || index >= this._size) return null;
    
    let current = this.head;
    
    for (let i = 0; i < index; i++) {
      current = current!.next;
    }
    
    return current!.value;
  }

其他功能函数

  // 转换为数组
  toArray(): T[] {
    const result: T[] = [];
    let current = this.head;
    
    while (current) {
      result.push(current.value);
      current = current.next;
    }
    
    return result;
  }

  // 清空链表
  clear(): void {
    this.head = null;
    this.tail = null;
    this._size = 0;
  }

  // 打印链表
  print(): void {
    let current = this.head;
    let output = "";
    
    while (current) {
      output += `${current.value} <-> `;
      current = current.next;
    }
    
    console.log(output + "null");
  }

思考

在removeAt方法里面,我们在删除某个元素是,此元素的pre和next并没有赋为null,只是把该元素的引用全都清空掉了,这时就需要提到 JS 里面的垃圾回收机制

在TypeScript/JavaScript中,内存管理是通过垃圾回收机制自动处理的。垃圾回收器会定期检查哪些对象不再被任何变量引用,然后释放这些对象占用的内存。

关键点:

  1. 引用计数:当一个对象没有被任何其他对象引用时,它就会被标记为可回收
  2. 循环引用处理:现代垃圾回收器能够检测并处理循环引用的情况
  3. 手动断开引用:虽然JS有自动垃圾回收,但手动断开不必要的引用可以加速内存释放