JS 之 常见线性结构 - 链表结构

文章详细介绍了链表的概念,将其比喻为火车结构,解释了链表与数组的不同实现方式。接着,文章列举了链表的常见操作,如append、insert、get、indexOf、update和remove等,并提供了JavaScript代码实现这些操作的示例。此外,还展示了如何封装链表结构和方法,以及给出了两个面试题,涉及链表的添加、删除和遍历等操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

1. 数组

2. 链表

链表和数组一样,可以用于存储一系列的元素,但是 链表和数组的实现机制完全不同
链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推

3. 链表常见操作

  • append(element):向链表尾部添加一个新的项

  • insert(position,element):向链表的特定位置插入一个新的项

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

  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1

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

  • removeAt(position):从链表的特定位置移除一项

  • remove(element):从链表中移除一项

  • isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false

  • size():返回链表包含的元素个数。与数组的length属性类似

二、代码实现

1. 封装链表结构

/**
 * 封装节点类
 */
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}
/**
 * 封装链表类
 */
class LickedList<T> {
  private head: Node<T> | null = null;
  private size: number = 0;
  get length() {
    return this.size;
  }
}

2. 封装链表方法

append - 追加方法

向链表尾部追加数据可能有两种情况:
  • 链表本身为空,新添加的数据时唯一的节点

  • 链表不为空,需要向其他节点后面追加节点

append(value: T) {
  // 1. 创建一个新的节点
  const newNode = new Node(value);

  // 2. 判断链表是否为空
  if (!this.head) {
    this.head = newNode;
  } else {
    // 3. 创建一个临时指针
    let current = this.head;
    while (current.next) {
      // 4. 指针往后挪移一次
      current = current.next;
    }
    // 5. 拼接到最后一个元素的next上
    current.next = newNode;
  }
  this.size++;
}

traverse - 遍历方法

traverse(): string {
  // 1. 定义结果数组
  const res: T[] = [];
  // 2. 定义临时指向头部初始指针
  let current = this.head;
  // 3. 当前节点存在时
  while (current) {
    // 4. 保存当前节点值
    res.push(current.value);
    // 5. 指针往后挪移一次
    current = current.next;
  }
  // 5. 返回拼接好的数据
  return res.join(' -> ');
}

inset - 插入方法

insert(position: number, value: T): boolean {
  // 1. 边界处理 :
  if (position < 0 || position > this.size) return false;
  // 2. 创建新节点
  const newNode = new Node(value);
  // 3. 头部位置插入 => 比较特殊
  if (position === 0) {
    // 注意 : 顺序不能出错
    newNode.next = this.head;
    this.head = newNode;
  } else {
    // 4. 创建临时指针
    let current = this.head!;
    // 5. 其他位置插入 => 让临时指针指向需更改元素的前一个位置
    for (let i = 0; i < position - 1; i++) {
      current = current.next!;
    }
    // 6. 保存原先指向
    const tempNode = current.next;
    // 7. 当前节点指向新节点
    current.next = newNode;
    // 8. 新节点指向原先的指向
    newNode.next = tempNode;
  }
  this.size++;
  return true;
}

removeAt - 删除方法

removeAt(position) :根据下标删除某个位置的元素
removeAt(position: number): Node<T> | null {
  // 0. 边界处理 :
  if (position < 0 || position >= this.size) return null;
  /**
   * 采用双指针
   */
  // 1. 指向当前节点
  let current = this.head;
  // 2. 指向前一个节点
  let privious: Node<T> | null = null;
  // 3. 删除头节点
  if (position === 0) {
    this.head = this.head!.next;
  } else {
    // 4. 删除其他位置节点
    // 5. 循环后,current指向指定位置的节点,privious指向前一个位置
    for (let i = 0; i < position; i++) {
      privious = current;
      current = current!.next;
    }
    // 6. 让前一个位置的next指向指定位置的next => 跳过当前节点
    privious!.next = current?.next ?? null;
  }
  this.size--;
  return current;
}

get - 获取方法

get(position) :获取对应位置的元素的值
get(position: number): T | null {
  // 0. 边界值
  if (position < 0 || position >= this.size) return null;
  // 1. 指向头节点
  let current = this.head;
  for (let i = 0; i < position; i++) {
    current = current!.next;
  }
  // 2. 返回查找到的节点
  return current!.value;
}

updata - 修改方法

update(position,element) :修改某个位置的元素的值
update(position: number, value: T): boolean {
  // 0. 边界值
  if (position < 0 || position >= this.size) return false;
  let current = this.head;
  // 1. 查找到指定节点
  for (let i = 0; i < position; i++) {
    current = current!.next;
  }
  // 2. 修改数据
  current!.value = value;
  return true;
}

indexOf - 查找索引

indexOf(position) :根据元素获取它在链表中的位置
indexOf(value: T): number {
  // 1. 指向头节点 
  let current = this.head;
  // 2. 初始下标
  let index = -1;
  // 3. 当节点存在时
  while (current) {
  // 4. 下标值加一
    index++;
  // 5. 判断是否查询到该元素
    if (current.value === value) {
      // 6. 查询到直接返回
      return index;
    }
  // 6. 没查询到,重新循环
    current = current.next;
  }
  // 7. 没有找到,返回-1
  return -1;
}

remove - 删除方法

remove(value) :获取元素的值删除元素
remove(value: T): Node<T> | null {
  // 1. 查找到对应位置
  const index = this.indexOf(value);
  // 2. 删除元素
  return this.removeAt(index);
}

3. 封装与重构

/**
 * 封装节点类
 */
class Node<T> {
  value: T;
  next: Node<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}
/**
 * 封装链表类
 */
class LickedList<T> {
  head: Node<T> | null = null;
  private size: number = 0;
  get length() {
    return this.size;
  }
  // 封装私有方法,根据位置获取索引
  private getNode(position: number): Node<T> | null {
    let current = this.head;
    for (let i = 0; i < position; i++) {
      current = current!.next;
    }
    // 2. 返回查找到的节点
    return current;
  }
  
  // 追加
  append(value: T): void {
    // 1. 创建一个新的节点
    const newNode = new Node(value);

    // 2. 判断链表是否为空
    if (!this.head) {
      this.head = newNode;
    } else {
      // 3. 创建一个临时指针
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      // 4. 拼接到最后一个元素的next上
      current.next = newNode;
    }
    this.size++;
  }

  // 遍历
  traverse(): string {
    // 1. 定义结果数组
    const res: T[] = [];
    // 2. 定义临时指向头部初始指针
    let current = this.head;
    // 3. 当前节点存在时
    while (current) {
      // 4. 保存当前节点值
      res.push(current.value);
      // 5. 指针往后挪移一次
      current = current.next;
    }
    // 5. 返回拼接好的数据
    return res.join(' -> ');
  }

  // 插入
  insert(position: number, value: T): boolean {
    // 1. 边界处理 :
    if (position < 0 || position > this.size) return false;

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

    // 3. 头部位置插入 => 比较特殊
    if (position === 0) {
      // 注意 : 顺序不能出错
      newNode.next = this.head;
      this.head = newNode;
    } else {
      // 4. 其他位置插入
      // 5. 获取指定位置前一个节点
      const previous = this.getNode(position - 1)!;
      // 6. 新节点指向指定位置节点
      newNode.next = previous.next;
      // 前一个节点指向新节点
      previous.next = newNode;
    }
    this.size++;
    return true;
  }

  // 根据下标删除元素
  removeAt(position: number): Node<T> | null {
    // 0. 边界处理 :
    if (position < 0 || position >= this.size) return null;
    /**
     * 采用双指针
     */
    // 1. 指向当前节点
    let current = this.head;
    // 2. 删除头节点
    if (position === 0) {
      this.head = this.head!.next;
    } else {
      // 3. 删除其他位置节点
      // 4. 获取指定位置前一个节点
      const previous = this.getNode(position - 1)!;
      // 6. 让前一个位置的next指向指定位置的next => 跳过当前节点
      previous!.next = previous?.next?.next ?? null;
    }
    this.size--;
    return current;
  }

  // 根据索引获取节点的值
  get(position: number): T | null {
    // 0. 边界值
    if (position < 0 || position >= this.size) return null;
    // 1. 获取当前节点
    const current = this.getNode(position)!;
    // 2. 返回查找到的节点
    return current.value;
  }

  // 根据索引更新节点的值
  update(position: number, value: T): boolean {
    if (position < 0 || position >= this.size) return false;
    // 1. 找到节点
    let current = this.getNode(position);
    // 修改值
    current!.value = value;
    return true;
  }

  // 根据值查找节点的索引
  indexOf(value: T): number {
    // 1. 指向头节点
    let current = this.head;
    // 2. 初始下标
    let index = -1;
    // 3. 当节点存在时
    while (current) {
      // 4. 下标值加一
      index++;
      // 5. 判断是否查询到该元素
      if (current.value === value) {
        // 6. 查询到直接返回
        return index;
      }
      // 6. 没查询到,重新循环
      current = current.next;
    }
    // 7. 没有找到,返回-1
    return -1;
  }

  // 根据节点的值删除节点
  remove(value: T): Node<T> | null {
    // 1. 查找到对应位置
    const index = this.indexOf(value);
    // 2. 删除元素
    return this.removeAt(index);
  }

  // 查看链表是否为空
  isEmpty(): boolean {
    return this.size === 0;
  }
}

三、面试题

面试题 - 设计链表

class Node {
  value: number;
  next: Node | null = null;
  constructor(value: number) {
    this.value = value;
  }
}

class MyLinkedList {
  private head: Node | null = null;
  private size: number = 0;
  get(index: number): number {
    let current = this.head;
    let currentIndex = -1;
    while (current) {
      currentIndex++;
      if (currentIndex === index) {
        return current.value;
      }
      current = current.next;
    }
    return -1;
  }

  addAtHead(val: number): void {
    const newNode = new Node(val);
    let current = this.head;
    if (!current) {
      this.head = newNode;
    } else {
      newNode.next = this.head;
      this.head = newNode;
    }
    this.size++;
  }

  addAtTail(val: number): void {
    const newNode = new Node(val);
    let current = this.head;
    if (!current) {
      this.head = newNode;
    } else {
      while (current.next) {
        current = current!.next;
      }
      current.next = newNode;
    }
    this.size++;
  }

  addAtIndex(index: number, val: number): void {
    if (index < 0 || index > this.size) return;
    const newNode = new Node(val);
    let current = this.head;
    if (index === 0) {
      newNode.next = current;
      this.head = newNode;
    } else {
      // 找到前一个元素
      for (let i = 0; i < index - 1; i++) {
        current = current!.next;
      }
      newNode.next = current!.next;
      current!.next = newNode;
    }
    this.size++;
  }

  deleteAtIndex(index: number): void {
    if (index < 0 || index >= this.size) return;
    let current = this.head;
    if (index === 0) {
      this.head = this.head!.next;
    } else {
      // 找到前一个元素
      for (let i = 0; i < index - 1; i++) {
        current = current!.next;
      }
      current!.next = current?.next?.next ?? null;
    }
    this.size--;
  }
  traverse() {
    let res: number[] = [];
    let current = this.head;
    while (current) {
      res.push(current.value);
      current = current.next;
    }
    return res.join(' -> ');
  }
}

export {}

面试题 - 删除链表中的节点

function deleteNode(node: ListNode | null): void {
    // 1. 把传过来的节点的值,改成下一个节点的值
    node!.val = node!.next!.val;
    // 2. 把当前节点的next,指向下下个节点
    node!.next = node!.next!.next;
    // 3. 没错!!!实际上是把下一个节点删了
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值