算法基础 - 04

235 阅读3分钟

基础数据结构

算法:串联整体逻辑的逻辑流 - 其依赖底层的数据结构

数据结构:数组-列表 栈-队列 哈希 树

image.png

  1. 数组 & 链表 [1,2,3,4]

image.png

链表的头为head,尾为null

  • 共性 --> 展示上都是一串元素的顺序集合

  • 不同 数组: 存储上都是以顺序存储的方式,即保存的时候一定是地址连续存储的,相邻两个元素是连续的,并可以通过index定位到。

    链表: 储存上元素地址不一定连续,不可以通过index获取某个值,但存在next指针指向下一个值。好处是可以知道下一个值会指向哪里。

总结:

查找:数组连续,效率高。可以迅速定位到数组中某一个节点位置;而链表则需要通过前一个元素指向下一个地址,需要前后依赖,效率低。 插入:数组中元素插入会引起后被插入位后所有元素索引的改变;而链表只需要改变某一个节点的next。(如在2,3间插入,改变2的next指针指向当前元素,再将当前元素的next指向3即可)

面试题: 实现链表/链表类 思路:head => node1 => node2 => ... => null

链表类结构:

  class LinkList {
    constructor () {
      this.length = 0;
      // 默认没有元素,那么头就指向尾
      this.head = null; // 可以用作链表是否为空的判断
    }

    // 一般会有的方法
    getElementAt(position) {

    } // 返回索引对应的元素

    append (element) {} // 添加节点

    insert (position, element) {} // 指定位置添加节点

    removeAt (position) {} // 删除指定位置元素

    indexOf (element) {} // 查找元素索引

    // remove 直接删除
    // isEmpty 是否为空
    // size 当前链表大小
    // getHead 获取当前链表的头
    // clear/reset 重制
  }

具体实现:

  getElementAt(position) {
    // 1. 边缘/安全检测
    if (position < 0 || position >= this.length) return null;

    // 本地变量
    let _current = this.head; // 保存当前的头

    // 查找当前链表,是否有当前的节点
    for (let i = 0; i < position; i ++) {
      _current = _current.next;
    }
    return _current;
  } 

  // 组装标准链表节点的辅助类
  class Node {
    constructor(element) {
      this.element = element;
      // 默认不指向任何地方
      this.next = null;
    }
  }

  // 生成复杂元素node
  append(element) {
    let node = new Node(element);
    
    // 添加时两种情况:链表为空或者非空

    // 链表为空
    if (this.head = null) {
      this.head = node;
    } else {
      // 不为空,找到尾巴
      let _current = this.getElementAt(this.length - 1);

      _current.next = node;
    }

    this.length ++;
  }

  insert(position, element) {
    if (position < 0 || position > this.length) return false;

    let node = new Node(element);

    if (position === 0) {
      node.next = this.head;
      this.head = node;
    } else {
      let previous = this.getElementAt(position - 1);

      node.next = previous.next;
      previous.next = node;
    }

    this.length ++;
    return true;
  }

  removeAt(position) {
    if (position < 0 || position > this.length) return false;

    let _current = this.head;

    if (position === 0) {
      this.head = _current.next;
    } else {
      let previous = this.getElementAt(position - 1);

      _current = previous.next;
      previous.next = _current.next;
    }

    this.length --;
    return _current.element;
  }

  indexOf(element) {
    let _current = this.head;

    for (let i = 0; i < this.length; i++) {
      if (current.element === element) return i;

      current = current.next;
    }

    return -1;
  }

双向链表 head <=> node1 <=> node2 <=> ... <=> null(tail) 多了一个tail和prev 面试:实现一个双向链表 思路:单向链表做继承

  class DoubleLink extends LinkList {
    // ...
  }
  1. 栈 & 队列

image.png

执行顺序不同:

  • 栈: 先入后出。类似一个盒子,放入123,拿出321 (队列:先入显出。流水线

面试题:实现一个栈

  class Stack {
    constructor() {
      this.items = [];
    }

    // 添加新元素到栈
    push(element) {
      this.items.push(element);
    }

    // 移除栈顶元素
    pop() {
      return this.items.pop();
    }

    // 获取栈顶元素
    getPeak() {
      return this.items[this.items.length -1];
    }

    // 判断空
    isEmpty() {
      return this.items.length === 0;
    }

    clear() {
      this.items = [];
    }

    size() {
      return this.items.length;
    }
  }
  1. 堆 heap JS中有两种存储方式:stack & heap 普通类型数据:在栈内直接存储值 高级引用类型:在栈内存储内存地址,地址指向堆,堆中存放具体的值

image.png

为什么这么存放?

对象和数组的数据大小较大,性能考量 函数的执行也在栈内

JS 执行 及 异步

image.png

面试题:判断括号有效性?(自闭合)

  // input: '{}[]' => true, '{{}[]' => false, '[{{}}]' true
  
  const isValid = function (s: string) {
    // 涉及使用数据结构 - 栈
    // 原因:栈的结构,单向,先放入一个,再放入一个,如果能匹配到,拿出;如果匹配不上,继续往下放
    const stack = new Stack();

    // 检查对应关系
    const map = {
      '}': '{',
      ']': '[',
      ')': '('
    }

    for(let i = 0; i < s.length; i++) {
      // 1. 逐个入栈
      const char = s[i];
      
      stack.push(char);

      if (stack.size() < 2) continue;

      // 2. 判断最后两个是否配对
      const theLastOne = stack.items[stack.size -1];
      const theLastTwo = stack.items[stack.size -2];

      if (map[theLastOne] === theLastTwo) {
        stack.pop();
        stack.pop();
      }
    }
    
    // 3. 衡量闭合有效性
    return stack.size() === 0;
  };
  1. 哈希:快速匹配定位 密码、罗马文、回文

面试:罗马文转数字 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 VI 6 IV 4

  const MAP = {
    'I': 1,
    'V': 5,
    'X': 10,
    'L': 50,
    'C': 100,
    'D': 500,
    'M': 1000
  }

  const romanToInt = function(s: string) {
    let len = s.length;
    let res = 0; 
    let max = 0;

    while(len--) {
      let num = MAP[s[len]];

      // 特殊情况 IV 颠倒值不直接相加,用大的
      if (max > num) {
        res -= num;
        continue;
      }
      max = num;
      res += num;
    }
    return res;
  }
  1. 树 基础概念: 查找方式:深度优先、广度优先 查找顺序:3种遍历方式
  2. 前序遍历:中左右 5-4-1-2 6-7-8
  3. 中序遍历:左中右 1-4-2-5 7-6-8
  4. 后序遍历:左右中 1-2-4 7-8-6 5

image.png

树结构

遍历顺序实现:

  // 前序遍历 中左右
  const PreOrder = function(node) {
    if (node !== null) {
      console.log(node);
      PreOrder(node.left);
      PreOrder(node.right);
    }
  }

  // 中序遍历 左中右
  const InOrder = function(node) {
    if (node !== null) {
      InOrder(node.left);
      console.log(node);
      InOrder(node.right);
    }
  }

  // 后续遍历 左右中
  const PostOrder = function(node) {
    if (node !== null) {
      PostOrder(node.left);
      PostOrder(node.right);
      console.log(node);
    }
  }

查找最大值、偶数层、拍平--Array.flat()