前置知识

前端数据及结构 - 链表 - 单向链表👍🏻

  • 常规数组与JS数组浅析

在大多数计算机语言中,数组都对应着一段连续的内存,JS中的常规数组也是这个标准(数组中存储的是同一种类型的数据),而当存储的是不一样类型的数据时,就会出现不是一段连续的内存的问题,此时JS数组就不再具备数组的特征了,其底层使用哈希映射分配内存空间,是由对象链表来实现的 → JS数组未必是真正的数组

常见数据结构

数组

是一种连续的存储结构,通过下标可以快速的直接访问数组中的任意元素,其时间复杂度是:随机访问(O(1)),插入和删除操作的时间复杂度是(O(N)),其空间复杂度是(需要连续的存储空间):空间复杂度是(O(N))

  • 数组的创建方式
  • const arr = [1, 2, 3]
  • 构造函数方式👍🏻:const arr = new Array()
  • const arr = new Array(7) //长度为7的空数组
  • 长度确定且元素值也确定的数组:const arr = (new Array(7)).fill(1)
  • 数组的遍历和访问
  • 访问直接通过索引进行访问即可
  • 遍历的三种常用方法
  • for循环
let arr = [1, 2, 3, 4];
let i = 0,
  length = arr.length;
for (i; i < length; i++) { ... }
  • forEach循环 → 从性能上来看 for循环遍历是最快的
let arr = [1, 2, 3, 4];
arr.forEach((item, index) => {
  console.log(item, index, "item,index ======== ");
});
  • map循环 → 对数组进行再加工
let arr = [1, 2, 3, 4];
arr.map((item,index) => {
    return item+index
})
  • 二维数组
  • 简单说就是数组嵌套数组,即内部的元素也是数组
  • 例如let arr = [[1, 2, 3], [3, 4, 5], [6, 7, 8]]
  • 二维数组的初始化与访问
  • 不能使用fill进行填充,会有「引用指向」的问题,如const arr = (new Array(7)).fill([])
  • 拓展:fill工作原理
  • 当给fill传入参数时,如果该入参是引用类型,那么fill在填充时填入的就是入参的引用,所有的填充都指向了同一块内存空间,本质上是同一个引用类型的数据,当更改二维数组中的某一个数据时会对其他数据也产生影响
  • for循环的方式进行初始化
let arr = new Array(6)
const len = arr.length;
let i = 0;
for (i; i < len; i++) {
  // 将数组的每一个坑位初始化为数组
  arr[i] = [];
}
arr[1][0] = 123
arr[4][1] = 234
  • 访问和一维数组一致,通过for循环进行访问,几维数组就嵌套几层for循环
  • 数组增加元素的方式
  • unshift → 添加元素到数组头部
  • arr.unshift(123)
  • push → 添加元素到数组的尾部
  • arr.push(321)
  • splice → 添加元素到数组的任意位置
  • arr.splice(1,0,3) // 在索引为1的位置处添加值为3的数据
  • arr.splice(1,1) // 删除索引为1位置的数据
  • 数组删除元素的方式
  • shift → 删除数组头部的元素
  • arr.shift()
  • pop → 删除数组尾部的元素
  • arr.pop()
  • splice → 删除任意位置的数据
  • arr.splice(1,1) // 删除索引为1位置的数据
  • 常用数组API对原始数组的影响汇总
  • 不改变原始数组:concat / filter / join / slice / reduce / findIndex / forEach / map
  • 改变原始数组:pop / push / shift / unshift / splice / reverse / sort

栈和队列可以看作是特别的数组,一般都依赖于数组,是运算受限的线性表,当然也可以用链表来实现,只是相当于是山鸡用了宰牛刀,两者的区别在于各自对数组的增删操作有着不一样的限制

栈结构

栈是一种遵从后进先出(LIFO)原则的有序集合,是限定仅在表尾(栈顶)进行插入和删除操作的线性表。新添加或待删除的元素都保存在栈的同一端,称为栈顶,另一端称为栈底,如现实中的「叠放盘子」

  • 注意点
  • 栈是一种线性表,即栈元素具有线性关系,即前驱后继关系
栈的实现
  • 拓展
  • 当一个需求有多种实现方式,每个实现方式都有公共的内部逻辑或统一的要求,此时就可以定义一个接口,然后让其他的类去继承implements这个接口(interface)
  • 继承implements:当有些方法可以在父类里进行实现,此时就可以使用继承来便捷实现
  • 当有些许不一样的地方时,就可以用接口interface的方式进行实现
  • 接口需要具有「通用性」,可以在不同场景下都适应,例如可以整合后续实例调用时所传递的不同接口类型的数据,这些数据都同时满足定义方法时规定的接口类型即可
  • 接口之间通过extend来互相继承
  • 类可以通过implements来实现继承接口
  • 继承和接口都有一个特点就是:都是「多态」的前提
实现栈结构

接口定义

  • 基类接口
// types/IList.ts

interface IList<T> {
    // peek  返回第一个元素
    peek(): T | undefined
    // 是否为空
    isEmpty(): boolean
    // 元素个数
    // 添加 get 后就可以直接以属性的方式进行调用该方法 是class的一个语法
    // 同步在继承中也需要改为get
    get size(): number
    // size(): number
}

export default IList
  • 栈的接口定义 → 继承自基类接口
// 栈结构Stack/IStack.ts

import IList from "../types/IList";
interface IStack<T> extends IList<T> {
  push(element: T): void;
  // 出对操作  不需要传参数  当队列为空时就应该返回undefined 因此用| undefined
  pop(): T | undefined;
  
  // 下列方法通过`extends IList`来实现 不再需要单独定义
  
  // // peek  返回第一个元素
  // peek(): T | undefined
  // // 是否为空
  // isEmpty(): boolean
  // // 元素个数
  // // get size(): number
  // size(): number
}
  • 实现栈结构(Stack) → (数组方式)
// 栈结构Stack/实现栈结构(Stack).ts

// 封装一个栈
// T 即是一个泛型  可以在创建类时告知需要创建的数据类型
import IStack from "./IStack";

// class ArrayStack<T = any> {
class ArrayStack<T = string> implements IStack<T> {
  // 属性修饰:protected 和 private 都是用来保护内部属性的 private只能自己访问不能外界访问 protected是可以让子类和自身都访问不能让外界访问
  private data: T[] = [];

  push(element: T): void {
    this.data.push(element);
  }

  // 将栈顶元素弹出栈  返回出该值 且从栈顶移除掉
  // 加「 | undefined」是因为pop的时候栈结构为空时 pop返回的是undefined  因此需要「联合类型」进行兼容这种情况
  pop(): T | undefined {
    return this.data.pop();
  }

  // peek  栈顶元素 不操作栈
  peek(): T | undefined {
    return this.data[this.data.length - 1];
  }

  isEmpty(): boolean {
    return this.data.length === 0;
  }

  get size(): number {
    return this.data.length;
  }
}

export default ArrayStack;
  • 实现栈结构(Stack) → (链表方式)
  • 测试栈结构
// 栈结构Stack/测试栈结构.ts

import ArrayStack from "./实现栈结构(Stack)";
// 栈:先进后出  开口端是栈顶 封口端是栈底
const stack = new ArrayStack<number>();

stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);

console.log(stack.peek(), "stack.peek()========="); // 4
console.log(stack.pop(), "stack.pop()========="); // 4
console.log(stack.pop(), "stack.pop()========="); // 3
队列结构

队列是一种遵循先进先出(FIFO)原则的一组有序的项,队列在尾部添加新元素,在顶部删除元素,最新添加的元素必须排在队列的末尾,现实中的例子如「排队」

实现队列结构
  • 队列的接口定义 → 继承自基类接口
// 队列结构Queue/IQueue.ts

import IList from "../types/IList";
interface IQueue<T> extends IList<T> {
  // 入队操作  不需要返回值 因此使用「void」
  enqueue(element: T): void;
  // 出对操作  不需要传参数  当队列为空时就应该返回undefined 因此用| undefined
  dequeue(): T | undefined;
  // // peek  返回第一个元素
  // peek(): T | undefined
  // // 是否为空
  // isEmpty(): boolean
  // // 元素个数
  // // 添加 get 后就可以直接以属性的方式进行调用该方法 是class的一个语法
  // // 同步在继承中也需要改为get
  // get size(): number
  // // size(): number
}

export default IQueue;
  • 实现队列结构Queue
// 队列结构Queue/实现队列结构Queue.ts

import IQueue from "./IQueue";

class ArrayQueue<T> implements IQueue<T> {
  // 内部是通过数组(链表)进行保存的
  private data: T[] = [];
  enqueue(element: T): void {
    this.data.push(element);
  }
  dequeue(): T | undefined {
    return this.data.shift();
  }
  peek(): T | undefined {
    return this.data[0];
  }
  isEmpty(): boolean {
    return this.data.length === 0;
  }
  // 添加 get 后就可以直接以属性的方式进行调用该方法 是class的一个语法
  get size(): number {
    return this.data.length;
  }
}

export default ArrayQueue;
  • 测试队列结构
// 队列结构Queue/测试队列结构.ts

import ArrayQueue from "./实现队列结构Queue";
// 队列:先进先出  队列两端开口  一端入队 一端出对
// 队列里准备放 String 格式的数据
const queue = new ArrayQueue<string>();

queue.enqueue("a");
queue.enqueue("b");
queue.enqueue("c");
queue.enqueue("d");

console.log(queue.dequeue(), "queue.dequeue()========="); // a
console.log(queue.dequeue(), "queue.dequeue()========="); // b
console.log(queue.peek(), "queue.peek()========="); // c
console.log(queue.isEmpty(), "queue.isEmpty()========="); // false
console.log(queue.size, "queue.size()========="); // 2
  • 面试题一:击鼓传花
import ArrayQueue from "./实现队列结构Queue";
function hotPotato(names: string[], num: number): number | object {
  if (names.length === 0) return -1;
  // 创建队列  用于存储数据  方便后续的条件化操作
  let queue = new ArrayQueue<string>();

  for (const name of names) {
    queue.enqueue(name);
  }
  // 完成队列的初始化

  // 淘汰规则
  while (queue.size > 1) {
    // 在指定num下的数据不需要进行删除  需要将其先出队  然后再入队
    for (let i = 0; i < num; i++) {
      const name = queue.dequeue();

      name && queue.enqueue(name);
    }
    // 不满足条件的需要出对 删除掉
    queue.dequeue();
  }

  // 找到满足条件的数据  进行格式化输出
  const remainResult = queue.dequeue();
  // const index = remainResult?(names.indexOf(remainResult)):-1
  const index = names.indexOf(remainResult!); //加「!」是断言的作用 效果类似上面注释掉的语句
  return { name: remainResult, index };
}

const remainIndex = hotPotato(["why", "james", "kobe", "curry", "lbxin"], 2);
console.log(remainIndex, "remainIndex=========");
// { name: 'curry', index: 3 } remainIndex=========
链表结构
  • 优点
  • 内存空间不必是连续的,可以实现较为灵活的内存动态管理
  • 链表不必在创建的时候就确定大小,而且可以无限的延伸下去
  • 链表在插入和删除数据时,时间复杂度可以达到O(1)
  • 缺点
  • 链表在访问任何一个数据时,都需要从头开始访问,无法跳过其中某个元素进行访问
  • 无法通过下标直接访问元素,需要从头开始一个一个进行访问
  • 具体内部实现原理
  • 链表中每个节点的构造需要包含数据域指针域,在JS中是通过嵌套对象来实现的
{
    // 数据域
    val: 1,
    // 指针域
    next: {
        val: 1,
        next: ...
    }
}
  • 在实现链表时需要封装两个类
// 需要封装两个类
class Node {
    value: any
    next: Node | null = null //最后一个node的next为空
}

class LinkedList {
    head: Node | null = null   
    append: (value:T) => {}    
    ......
}
实现链表
  • 封装Node类 用于创建符合规则的Node节点
class Node<T> {
  value: T;
  next: Node<T> | null = null; //联合类型 防止有空值的存在
  constructor(value: T) {
    this.value = value;
  }
  // constructor(public value: T) {}    // 等同于 25行和27~29行的简写  TS语法
}
  • 实现基类接口
// types/IList.ts

interface IList<T> {
  // peek  返回第一个元素
  peek(): T | undefined;
  // 是否为空
  isEmpty(): boolean;
  // 元素个数
  // 添加 get 后就可以直接以属性的方式进行调用该方法 是class的一个语法
  // 同步在继承中也需要改为get
  get size(): number;
  // size(): number
}

export default IList;
  • 链表接口的定义 → 继承自基类接口
// 链表结构LinkedList/ILinkList.ts

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;
  // peek  返回第一个元素
  // peek(): T | undefined
  // 是否为空
  // isEmpty(): boolean
  // 元素个数
  // get size(): number
  // size(): number

  get length(): number;
}

export default ILinkedList;
  • 实现链表
import ILinkedList from "./ILinkList";
class Node<T> {
  value: T;
  next: Node<T> | null = null; //联合类型 防止有空值的存在
  constructor(value: T) {
    this.value = value;
  }
  // constructor(public value: T) {}    // 等同于 25行和27~29行的简写  TS语法
}

// 创建LinkedList类
class LinkedList<T> implements ILinkedList<T> {
  head: Node<T> | null = null; //头结点
  length: number = 0;
  // length为了实现数组和链表转化时的共有属性方法的兼容问题
  get size(): number {
    return this.length;
  }
  peek(): T | undefined {
    return this.head?.value;
  }
  // 封装私有方法
  // 根据position获取到当前节点
  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) {
    // 根据value创建新节点
    const newNode = new Node(value);

    if (this.head === null) {
      this.head = newNode;
    } else {
      // 不为空 需要找到最后一个节点 然后将最后一个节点的next指向该新节点
      // 解决办法是创建一个临时节点  用于找到最后一个节点后将其指向最后一个节点
      let current = this.head;
      while (current.next) {
        current = current.next;
      }

      // current肯定是指向最后一个节点的
      current.next = newNode;
    }

    this.length++;
  }

  // 遍历列表
  traverse() {
    // 现将当前的节点设置为链表的头结点
    let current = this.head;

    // 定义最终输出数据的格式缓冲区
    let values: T[] = [];

    // 遍历链表 打印出当前节点的数据
    while (current) {
      // console.log(current.value,'current.value=========')
      values.push(current.value);

      // 操作完当前节点后 将当前节点设置为「下一个节点」 知道遍历完整个链表
      current = current.next;
    }
    console.log(values.join(" -> "), "values.join()=========");
  }

  // 插入方法
  insert(value: T, position: number) {
    if (position < 0 || position > this.length) {
      // throw new Error(`插入的${position}越界了!`)
      return false; //上面的方式会影响项目的运行
    }

    // 根据value创建新节点
    let newNode = new Node(value);

    // 是否是插入到头部
    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 {
    // 越界判断
    if (position < 0 || position >= this.length) return null;

    let current = this.head;
    if (position === 0) {
      this.head = current?.next ?? null; //可选链
    } else {
      let previous = this.getNode(position - 1);
      current = previous?.next ?? null; 
      previous!.next = previous?.next?.next ?? null;
    }
    this.length--;
    console.log(current?.value, "current=========");
    return current?.value ?? null;
  }

  get(position: number): T | null {
    if (position < 0 || position >= this.length) return null;
    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;
  }

  remove(value: T): T | null {
    let posrtion = this.indexOf(value);
    return this.removeAt(posrtion);
  }

  isEmpty(): boolean {
    return this.size === 0;
  }
}

export { LinkedList }; //为了解决名称冲突 可以放入到一个模块里
  • 面试题:反转链表
// 链表结构LinkedList/utils.ts

// 面试题基类Node封装
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 { ListNode };
  • 栈结构方式反转
import { ListNode } from "./utils";
// 栈结构的缺点是会创建了一个内存单元去存储数据
function reverseList(head: ListNode | null): ListNode | null {
  // 不需要处理的情况 - head为null
  if (head == null) return null;
  // 不需要处理的情况 - 只有一个节点 即head.next为null
  if (head.next === null) return head;

  // 栈的方式 - 数组模拟栈结构
  const stack: ListNode[] = [];
  let current: ListNode = head!;
  while (current) {
    stack.push(current);
    current = current.next!;
  }
  console.log(stack, "stack=========");
  // 依次从栈结构中取出数据  放到一个链表中
  const newHead: ListNode = stack.pop()!;
  let newHeadCurrent = newHead;
  while (stack.length) {
    const node = stack.pop()!;
    newHeadCurrent.next = node;
    newHeadCurrent = newHeadCurrent.next;
  }

  newHeadCurrent.next = null;
  return newHead;
}

// 测试
const node1 = new ListNode(1);
node1.next = new ListNode(2);
node1.next.next = new ListNode(3);
console.log(node1, "node1=========");
const newHead = reverseList(node1);
let current = newHead;
while (current) {
  current = current.next;
}
console.log(newHead, "newHead=========");
// ListNode {
//     val: 1,
//     next: ListNode { val: 2, next: ListNode { val: 3, next: null } }
//   } node1=========
//   [
//     ListNode { val: 1, next: ListNode { val: 2, next: [ListNode] } },
//     ListNode { val: 2, next: ListNode { val: 3, next: null } },
//     ListNode { val: 3, next: null }
//   ] stack=========
//   ListNode {
//     val: 3,
//     next: ListNode { val: 2, next: ListNode { val: 1, next: null } }
//   } newHead=========
export {};
  • 递归方式反转
import { ListNode } from "./utils";
let count = 1;
function reverseList(head: ListNode | null): ListNode | null {
  // 当使用递归时  需要有明确的结束条件  不然会陷入死循环的问题
  // if(head === null) return null
  // if(head.next === null) return head
  if (head === null || head.next === null) return head;
  // 创建newNode时最后一次返回的是最后一个节点  每次循环都会返回最后一个节点
  const newNode = reverseList(head.next);
  // 完成了递归需要处理的操作 首次来到这个位置的时候是倒数第二个节点
  // 这里打印newNode时 每次都是最后一个节点 不同之处在于最后一个节点在不断的反转后的next指向不同 第一次是null 第二次是倒数第二个
  console.log(newNode, `第${count++}次的节点是:`, head);
  head.next.next = head;
  head.next = null;
  return newNode;
}

// =========================== 测试代码 start ===========================
const node1 = new ListNode(1);
node1.next = new ListNode(2);
node1.next.next = new ListNode(3);
console.log(node1, "node1=========");
const newHead = reverseList(node1);
let current = newHead;
while (current) {
  console.log(current.val, "current.val=========");
  current = current.next;
}
console.log(newHead, "newHead=========");
// ListNode {
//     val: 1,
//     next: ListNode { val: 2, next: ListNode { val: 3, next: null } }
//   } node1=========
//   ListNode { val: 3, next: null } 第1次的节点是: ListNode { val: 2, next: ListNode { val: 3, next: null } }
//   ListNode { val: 3, next: ListNode { val: 2, next: null } } 第2次的节点是: ListNode { val: 1, next: ListNode { val: 2, next: null } }
//   3 current.val=========
//   2 current.val=========
//   1 current.val=========
//   ListNode {
//     val: 3,
//     next: ListNode { val: 2, next: ListNode { val: 1, next: null } }
//   } newHead=========
export {};