基础数据结构(三):线性结构(链表)使用typescript封装链表

42 阅读4分钟

链表和数组都可以存储一系列的数据,我们第一篇有说过,数组有着插入数据效率低、访问数据效率高的特点,而链表正好相反,它有着插入数据效率高、访问数据效率低的特点。
不关注数据结构的前端开发人员可能不熟悉链表,链表其实就像一个链条一样,是由一个个节点连接起来组成的结构,每个节点有一个value用于存储该节点的值,还有一个或两个引用指向上下节点(单链表和双向链表)。

image.png 由于链表节点由引用连接,所以链表节点在内存中不必是连续的,因此访问数据必须从头开始找,所以访问数据的效率比较低。而插入数据时,只需要改变节点指针的指向即可,所以插入的效率会很高。

封装链表

链表是由节点构成,所以我们先封装一下节点的类

class LinkedNode<T> {
  value: T;
  next: LinkedNode<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

然后我们先初始化一下链表的类

class LinkedList<T> {
  // 链表头部
  head: LinkedNode<T> | null = null;
  // 链表尾部
  tail: LinkedNode<T> | null = null;
  // 链表长度
  size: number = 0;
}

我们首先实现一下简单的插入节点的方法:从头部插入、从尾部插入

// 在链表头部添加元素
addAtHead(value: T) {
  const node = new LinkedNode(value);
  // 如果此时head为null head和tail都指向node
  if (this.head === null) {
    this.tail = node;
  }
  node.next = this.head;
  this.head = node;
  this.size++;
}
// 在链表尾部添加元素
addAtTail(value: T) {
  const node = new LinkedNode(value);
  // tail为null说明链表为空 设置head
  if (this.tail === null) {
    this.head = node;
  } else {
    this.tail.next = node;
  }
  this.tail = node;
  this.size++;
}

再实现获取index位置的node

// 获取index位置的元素 没有返回null
getNode(index: number) {
  // 边界判断
  if (index < 0 || index >= this.size) {
    return null;
  }
  let cur = this.head;
  for (let i = 0; i < index; i++) {
    cur = cur!.next;
  }
  return cur;
}

再实现从index位置插入node,如果index等于链表长度 则添加到链表尾部

addAtIndex(index: number, value: T) {
  // 边界判断
  if (index < 0 || index > this.size) {
    throw new Error("index out of range");
  }
  // 头部插入
  if (index === 0) {
    this.addAtHead(value);
    return;
  }
  // 尾部插入
  if (index === this.size) {
    this.addAtTail(value);
    return;
  }
  // index位置插入
  const prev = this.getNode(index - 1);
  const node = new LinkedNode(value);
  node.next = prev!.next;
  prev!.next = node;
  this.size++;
}

再实现删除index位置的node

deleteAtIndex(index: number) {
  // 边界判断
  if (index < 0 || index >= this.size) {
    throw new Error("index out of range");
  }
  if (index === 0) {
    this.head = this.head!.next;
    if (this.size === 1) {
      this.tail = null;
    }
  } else {
    // 删除index的节点只需要让上个节点的next指向下个节点即可
    const prev = this.getNode(index - 1);
    prev!.next = prev!.next!.next;
    if (index === this.size - 1) {
      // 删除最后一个节点时 更新tail指向
      this.tail = prev;
    }
  }
  this.size--;
}

到这里一个单向链表基本就封装完成了,其实还是比较简单的,如果你想要实现双向链表可以在这个基础上加上pre指针,感兴趣的话自己实现一下吧!

链表练习

删除链表中的节点

leetcode的第237题:有一个单链表的head,我们想删除它其中的一个节点node。给你一个需要删除的节点node。你将无法访问第一个节点head。链表的所有值都是唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是

  • 给定节点的值不应该存在于链表中。

  • 链表中的节点数应该减少 1。

  • node 前面的所有值顺序相同。

  • node 后面的所有值顺序相同。

刚看这道题你可能会很懵逼,不给head怎么删除?其实你认真审题就会发现,我们不需要把给定的node删掉,我们只需要把他的值替换成下一个节点的值,然后删掉下一个节点就可以了。搞清楚这一点其实就变得非常简单了,只需要两行代码

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

反转链表

leetcode206题:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 这道题相信我们拿到题目就应该知道怎么做了,遍历旧链表,拼装新链表即可

function reverseList(head: ListNode | null): ListNode | null {
  if (head === null || head.next === null) return head
  let cur = head;
  let revertHead = null
  let curRvt = null
  while(cur !== null) {
    revertHead = new ListNode(cur.val)
    revertHead.next = curRvt
    curRvt = revertHead
    cur = cur.next
  }
  return revertHead
};

除了使用遍历,其实效率更高的方法是使用递归。确认使用递归时我们要第一时间搞清楚递归函数的作用,这里是反转数组,那么我们假设把head.next传入reverseList,那么会得到这种效果:

image.png 以此类推,我们只需要处理reverseList的结果和head的指向关系:

function reverseList(head: ListNode | null): ListNode | null {
  // 递归结束条件
  if (head === null || head.next === null) return head;
  let reverse = reverseList(head.next);
  // 获取到reverse 处理与head的关系
  head.next.next = head;
  head.next = null;
  return reverse
};