Skip to content

12-3 数据结构与算法

统计信息:字数 14636 阅读30分钟

主要是下去自己多刷题

知识点

算法复杂度、算法稳定性(数组操作中是否改变原始数据,通常稳定性好的算法空间复杂度较高);常见算法和思路(冒泡排序,插入排序,快速排序-二分递归、数组flat、查找算法)数据结构(数组,队列、栈、树、哈希表、集合、链表、图、红黑树、DFS);动态规划,贪心算法等。

算法优劣

时间复杂度:大O表示法,二分取对数

空间复杂度:内存(减少内部缓存数组等)快速排序中使用指针代替左右数组

算法稳定性

冒泡排序

最差N平方,注意边界条件

function bubble(arr) {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        // 调换顺序
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
       // 可以使用普通调换顺序 let c = a; a = b; b = c; 调换顺序 
      }
    }
  }
}

快速排序

二分+递归 N * logN

可以借助两个辅助的数组

function quick(arr) {
  if (arr.length <= 1) return arr;
  let flag = arr.shift();
  let left = [];
  let right = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < flag) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return quick(left).concat(flag, ...quick(right));
}

或者借助两个辅助的指针完成(节省内存)

数组扁平化

使用递归处理

function flat(arr) {
  let res = [];
  arr.forEach((item, index) => {
    if (Array.isArray(item)) {
      res = res.concat(flat(item));
    } else {
      res.push(item);
    }
  });
  return res;
}

爬楼算法

有十个台阶,每次可以爬升1个或者2个,有多少中爬楼的情况

思路1:分治算法:首先看看有多少个情况的组合(外部循环)然后内部循环排列组合一下这些情况,不好

思路2:贪心算法(硬币找零问题)

可以使用递归的思路,返回来就是斐波那契数列。爬10个台阶的算法 = 爬8个台阶(最后一次两个台阶)+ 爬9个台阶(最后一次一个台阶) fn(x) = fn(x-1) + fn(x-2) 关键就是求初始化的几个值,后面的递归计算。中间增加一个列表存储已经计算的结果,优化算法。

let stair = function(n) {
  if (n < 0) {
    return 0;
  }
  else if (n === 0) {
    return 1;
  }
  else if (n > 0) {
    return stair(n - 1) + stair(n - 2)
  }
}

二分查找算法

二分法适应于已经排序好的数组,可以使用循环或者递归实现

循环实现

/**
 * [binary find]
 * @author Michael An
 * @DateTime 2021-10-28T17:44:09+0800
 * @param    {[array]}                 arr   [sorted arr]
 * @param    {[number]}                target
 * @return   {[number]}                [if find, return index, else return -1]
 */
function binary(arr, target) {
  let low = 0;
  let high = arr.length - 1;
  let mid;
  while (low < high) {
    mid = Math.floor((low + high) / 2);
    if (target === arr[mid]) {
      return (mid + 1);
    }
    if (target < arr[mid]) {
      high = mid - 1;
    }
    else if (target > arr[mid]) {
      low = mid + 1;
    }
  }
  return -1;
}

递归实现

function binary(arr, target, low = 0, high) {
  high = high || arr.length - 1;
  const n = Math.floor((low + high) / 2);
  const cur = arr[n];
  if (cur === target) {
    return (n + 1);
  } else if (cur > target) {
    return binary(arr, target, low, n - 1);
  } else if (cur < target) {
    return binary(arr, taregt, n + 1, high);
  }
  return -1;
}

判断括号是否成对

或者判断一段HTML是否完整,可以使用栈判断

先解析标签中的部分<>,然后把内容放入栈;然后匹配 ,从栈中导出对应的部分。

这里需要使用正则等获取标签部分。

我们使用数组模拟一个栈(实际上JS数组直接可以使用)

class Stack {
  constructor() {
    this.items = [];
  }
  push(item) {
    this.items.push(item);
  }
  pop() {
    return this.items.pop();
  }
  size() {
    return this.items.length;
  }
}

判断括号是否匹配

/**
 * [isBalance check a string is balance]
 * @author Michael An
 * @DateTime 2021-10-28T17:58:47+0800
 * @param    {[string]}                 symbol
 * @return   {Boolean} 
 */
function isBalance(symbol) {
  const stack = new Stack();
  const left = '{[(';
  const right = '}])';
  let flag = true;
  for (let i = 0; i < symbol.length; i++) {
    let s = symbol[i];
    if (left.includes(s)) {
      stack.push(s);
    }
    else if (right.includes(s)) {
      let res = stack.pop();
      if (left.indexOf(res) !== right.indexOf(s)) {
        flag = false;
        return flag;
      }
    }
  }
  if (stack.size() !== 0) {
    return false;
  }
  return true;
}

链表

链表存储一个数据+指针(fiber的架构就是链表 linkedList)

首先构建数据结构

class Node {
  constructor(element) {
    this.element = element;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.current;
    this.length = 0;
  }

  append(ele) {
    const node = new Node(ele);
    if (this.head = null) {
      this.head = node;
    } else {
      this.current = this.head;
      while (this.current.next) {
        this.current = this.current.next;
      }
      this.current.next = node;
    }
    this.length++;
  }

  // 其他API
}

集合

没有重复数据(set),可以使用数组模拟

class Set {
  constructor() {
    this.items = {};
  }
  has(value) {
    return this.items.hasOwnProperty(value);
  }
  add(value) {
    if (!this.has(value)) {
      this.items[value] = value;
      return true;
    }
    return false;
  }
  remove(value) {
    if (!this.has(value)) {
      delete this.items(value);
      return true;
    }
    return false;
  }
  getSize() {
    return Object.keys(this.items).length;
  }
  getValues() {
    return Object.keys(this.items);
  }
}

const set = new Set();

哈希表

使用对象模拟:键需要经过散列函数计算,获取散列值,然后对应堆内存地址

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

  }
  set(key, value) {
    const hash = this.keyToNum(key);
    this.items[hash] = value;
  }

}

散列函数,处理哈希碰撞(存储列表方式解决)相同的哈希值,放在一个列表中处理

根节点、子节点、DFS、二叉树

DOM 树(遍历树的方法)

let dom = '#app';
walk(dom, (node) => {
  console.log(node);
});

function walk(node, fn) {
  if (!node || !fn) return null;
  if (node instanceof window.Node) {
    _walk(node, fn);
  }
  return node;
}

function _walk(node, fn) {
  if (fn(node) !== false) {
    node = node.firstChild;
    while (node) {
      _walk(node, fn);
      node = node.nextSibling;
    }
  }
}

动态规划

动态规划:首先使用暴力解法 ->如果可以有更好的办法,那么使用更好的办法

贪心算法相反,优先使用性价比最好的策略(但是结果不一定是最好的);动态规划先使用性价比最低的方法,逐步计算逐步使用性价比较高的方法迭代之前的旧办法。

找零钱,背包问题可以使用这个算法。

例如,斐波那契数列

// 不断递归调用,会造成大量的重复计算
function fib(n) {
  if (n <= 0) return 0;
  if (n === 1 || n === 2) return 1;
  return fib(n - 1) + fib (n - 2);
}

我们先使用缓存优化算法

let dict = [];

function fib(n) {
  if (dict[n]) {
    return dict(n);
  } else {
    let result = fib(n - 1) + fib (n - 2);
    dict[n] = result;
    return result;
  }
}

动态规划:自下而上,先计算 fn(1), fn(2) 之后计算 fn(20)

function fib(n) {
  if (n === 1 || n === 2) return 1;
  let dp = [];
  dp[1] = dp[2] = 1;
  for (let i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
}

动态规划找零钱算法(记录每一步的缓存,判断下一步的最佳值)

class Change {
  constructor(changeType) {
    this.changeType = changeType;
    this.cache = {};
  }
  makeChange(amount) {
    let min = [];
    if (!amount) return [];
    // 从缓存中读取数量
    if (this.cache[amount]) {
      return this.cache[amount];
    }

    for (let i = 0; i < this.changeType.length; i++) {
      const leftAmount = amount - this.changeType[i];
      let newMin;
      if (leftAmount >= 0) {
        // 找零后,还有剩余,那么继续找零
        newMin = this.makeChange(leftAmount);
      }
      if (leftAmount >= 0 && (newMin.length < min.length - 1 || !min.length)) {
        // 动态规划(如果新的方案比旧的方案好,那么使用新的方法)
        min = [this.changeType[i]].concat(newMin);
      }
    }
    return this.cache[amount] = min;
  }
}

贪心算法

近似求解:当能满足大部分情况的时候,就认为逻辑正确。使用算法前,需要确定算法是否适用于这个问题。

class Change {
  constructor(changeType) {
    // 先把零钱排序
    this.changeType = changeType.sort((r1, r2) => r2 - r1);
  }

  makeChange(amount) {
    const arr = [];
    for (let i = 0; i < this.changeType.length; i++) {
      while (amount - this.changeType[i] >= 0) {
        arr.push(this.changeType[i]);
        amount = amount - this.changeType[i];
      }
    }
    return arr;
  }
}

const change = new Change([1,2,10,50,20,100]);

change(12345);

前端数据结构

virtual dom

fiber

Hooks

常见数据结构与算法

常用数据结构

数组:数据连续,可以通过索引随机访问。增加和删除复杂度N(需要改变元素的内存);根据索引查找复杂度1,根据值查找复杂度N。有序的存储方式。

链表:非连续的存储:增加删除都是1,查找是N。

哈希表:了解散列函数,哈希碰撞-解决方法是使用链表存储碰撞的元素(或者哈希数组扩容)。哈希表增删查的复杂度都是1,消耗内存空间。

let arr = new Array(10).fill('');
function hash(s) {
  return s.charCodeAt() * 37 % 10;
}

树:必须掌握(字符串转换成树,AST),DOM节点树的操作;

二叉树可以转换成数组。每一层都是2N次方。

左边的元素是 2N,右边的元素是 2N(次方) + 1

常用算法

排序(冒泡、快速)

搜索(二分)

回溯、贪心、动态规划

递归(数组打平、括号匹配,HTML标签匹配等、树计算)

let app = document.getElementById('app');
let fn = (node) => {console.log(node);};
walk(app, fn);

function walk(node, func = () => {}) {
  if (node instanceof window.Node) {
    _walk(node, func);
  }
  return node;
}

function _walk(node, func) {
  if (func(node) !== false) {
    node = node.firstChild;
    while(node) {
      _walk(node, func);
      node = node.nextSibling;
    }
  }
}

参考书

入门:我的第一本算法书、啊哈算法、图解HTTP,图解TCP

经典教材:算法-第四版(一本书看了三个月,习题全部看一次)

主题阅读法:看一个知识点,只学习这个知识点;不会被其他的打扰

不断刷题才能提高算法:LeetCode 按照标签一个一个学习

树是复杂的列表,所以把列表各种操作熟练,并在工作中使用这些知识点

常见算法知识点固定,目标明确

任何高级技能,需要长时间练习;我们常常放大短期学习的威力,缩小长期学习的威力。

习题

00 数组扁平化

递归

function flat(array) {
  let res = [];
  array.forEach((item) => {
    if (Array.isArray(item)) {
      res = res.concat(flat(item));
    } else {
      res.push(item);
    }
  });
  return res;
}

01 两数之和

twoSum 使用哈希表存储目标索引,循环一次

var twoSum = function(nums, target) {
  let obj = {};
  for (let i = 0; i < nums.length; i++) {
    let item = nums[i];
    if (item in obj) {
      return [obj[item], i]; // find two number and return index
    } else {
      obj[target - item] = i;
    }
  }
};

46 全排列(回溯算法)

function backtrack(list, temp, nums) {
  // 递归的结束条件
  if (temp.length === nums.length) {
    return list.push([...temp]);
  }
  for (let i = 0; i < nums.length; i++) {
    if (temp.includes(nums[i])) {
      continue;
    }
    // 回溯(放进去,执行,抛出来)
    temp.push(nums[i]);
    backtrack(list, temp, nums);
    temp.pop();
  }
}

var permute = function(nums) {
  let list = [];
  let temp = []
  backtrack(list, temp, nums);
  return list;
}

94 二叉树的中序遍历

/**
 * @param {TreeNode} root
 * @return {number[]}
 * 中序遍历:左根右
 */
var inorderTraversal = function(root) {
  if (!root || !root.val) {
    return [];
  }
  var list = [];
  runNode(root, list);
  return list;
};

function runNode(node, list) {
  if (node.left) {
    runNode(node.left, list)
  }
  list.push(node.val);
  if (node.right) {
    runNode(node.right, list)
  }
}

100 相同的树

var isSameTree = function(p, q) {
  if (p == null && q == null) {
    return true;
  } else if (p == null || q == null) {
    return false;
  } else if (p.val !== q.val) {
    return false;
  } else {
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
  }
};

判断节点的值是否相等;递归判断子节点是否相等

104 二叉树的最大深度

var treeDepth = (node) => {
  if (!node) {
    return 1;
  }
  return Math.max(treeDepth(node.left), treeDepth(node.right));
} 

141 环形列表

// 环形列表不能通过值记录,可能列表中存在重复值
// 下面适合没有重复元素的链表
var hasCycle = function(head) {
  if (!head) {
    return false;
  }
  let val = head.val;
  head.isRun = true;

  while (head && head.next) {
    head = head.next;
    val = head.val
    if (!head.isRun) {
      head.isRun = true;
    } else {
      return true;
    }
  }
  return false;
}

如何判断一个节点已经被遍历了?能否设置一个属性为true,这样会更改原始节点,看看有没有更好的方法。现在需要循环列表。最好的办法是设置一个快指针,一个慢指针,如果指针重复,那么就有环形结构。

下面是较难的回溯算法: 51-N皇后 8皇后


Last update: November 9, 2024