前端小白算法入门

151 阅读12分钟

常见问题

  1. 什么是数据结构?
    用于表示数据关系的结构
    常见的数据结构有:数组、链表、树、图等
  2. 什么是算法?
    根据已知数据得到结果数据的计算方法
    常见的算法有:穷举法、分治法、贪心算法、动态规划
  1. 数据结构和算法有什么关系?
    数据结构关心的是如何使用合适的结构存储数据
    算法关心的是计算过程
    有了相应的数据结构,免不了对这种数据结构的各种变化进行运算,所以,很多时候,某种数据结构都会自然而然的搭配不少算法。
  2. 数据结构和算法课程使用什么计算机语言?
    数据结构和算法属于计算机基础课程,它们和具体的语言无关,用任何语言都可以实现。
    本课程采用JavaScript语言。
  1. 配置要求
最低配置推荐配置
知识变量、判断、循环、数组、函数、 构造函数、值类型和引用类型除最低配置外,加上递归、作用域链、 执行上下文、执行栈
代码量500行3000行
练习量3个效果10个效果

递归知识回顾

递归(recursion)是函数式编程思想的产物,它使用数学函数的思想进行运算,只要在数学逻辑上是合理的,即代码中的函数一定合理

使用递归时,无须深究其运行过程!

斐波拉契数列的特点是:第1位和第2位固定为1,后面的位,其数字等于前两位之和,比如:

[1, 1, 2, 3, 5, 8, 13, 21, ......]

求斐波拉契数列第n位的值,n>0

如果使用函数f(n)来表示斐波拉契数列第n位的值,通过数学分析,可以轻松得到:

以上等式考虑到了所有情况,并且在数学逻辑上是合理的,因此,可以轻松书写到代码中:

f(1) = 1
f(2) = 1
f(n) = f(n-1) + f(n-2)
// 求斐波拉契数列第n位的值
function f(n){
  	if(n === 1 || n === 2){
    		return 1;
  	}
  	return f(n-1) + f(n-2);
}

1. 线性结构

线性结构是数据结构中的一种分类,用于表示一系列的元素形成的有序集合。

常见的线性结构包括:数组、链表、栈、队列

数组

特别注意:这里所说的数组是数据结构中的数组,和JS中的数组不一样

数组是一整块连续的内存空间,它由固定数量的元素组成,数组具有以下基本特征:

  1. 整个数组占用的内存空间是连续的
  2. 数组中元素的数量是固定的(不可增加也不可减少),创建数组时就必须指定其长度
  3. 每个元素占用的内存大小是完全一样的

根据数组的基本特征,我们可以推导出数组具有以下特点

  1. 通过下标寻找对应的元素效率极高,因此遍历速度快
  2. 无法添加和删除数据,虽然可以通过某种算法完成类似操作,但会增加额外的内存开销或时间开销
  1. 如果数组需要的空间很大,可能一时无法找到足够大的连续内存

JS中的数组

在ES6之前,JS没有真正意义的数组,所谓的Array,实际上是一个对象。

ES6之后,出现真正的数组(类型化数组),但是由于只能存储数字,因此功能有限

目前来讲,JS语言只具备不完善的数组(类型化数组)

www.cnblogs.com/xiewangfei1…

链表

为弥补数组的缺陷而出现的一种数据结构,它具有以下基本特征

  1. 每个元素除了存储数据,需要有额外的内存存储一个引用(地址),来指向下一个元素
  2. 每个元素占用的内存空间并不要求是连续的
  1. 往往使用链表的第一个节点(根节点)来代表整个链表

根据链表的基本特征,我们可以推导出它具有以下特点

  1. 长度是可变的,随时可以增加和删除元素
  2. 插入和删除元素的效率极高
  3. 由于要存储下一个元素的地址,会增加额外的内存开销
  4. 通过下标查询链表中的某个节点,效率很低,因此链表的下标遍历效率低

手动用代码实现链表

实际上,很多语言本身已经实现了链表,但链表作为一种基础的数据结构,通过手写代码实现链表,不仅可以锻炼程序思维和代码转换能力,对于后序的复杂数据结构的学习也是非常有帮助的。

因此,手写链表是学习数据结构和算法的一门基本功

手写一个链表结构,并完成一些链表的相关函数,要实现以下功能:

  function Node(value) {
    this.value = value;
    this.next = null;
  }

  var a = new Node("a");
  var b = new Node("b");
  var c = new Node("c");
  var d = new Node("d");

  a.next = b;
  b.next = c;
  c.next = d;
  
  1. 遍历打印
  function print(node){
    var n = node
    while(n) {
      console.log(n.value)
      n = n.next
    }
  }

  // 打印整个链表
  function print(node) {
    // 打印自己
    if (!node) {
      return; //没有节点,无法打印
    }
    console.log(node.value);
    // 打印下一个
    print(node.next);
  }
  1. 获取链表的长度
  // 获取链表的长度
  function getLength(node) {
    if (!node) {
      return 0; // 没有节点,长度为0
    }
    return 1 + getLength(node.next);
  }
  1. 通过下标获取链表中的某个数据
  // 通过下标获取链表中的某个数据
  function getValue(node, index) {
    // 根据当前节点和当前下标,得到对应的值
    function _getValue(curNode, curIndex) {
      if (!curNode) {
        return null; // 没有找到
      } else if (curIndex !== index) {
        return _getValue(curNode.next, curIndex + 1);
      } else {
        // 下标相等
        return curNode.value;
      }
    }
    return _getValue(node, 0);
  }
  1. 通过下标设置链表中的某个数据
	// 通过下标设置链表中的某个数据
  function setValue(node, index, value) {
    // 设置当前节点的值
    function _setValue(curNode, curIndex) {
      if (!curNode) {
        return; // 节点都没啦,停止
      } else if (curIndex === index) {
        // 找到了对应的节点
        curNode.value = value;
      } else {
        _setValue(curNode.next, curIndex + 1);
      }
    }

    _setValue(node, 0);
  }
  1. 在链表末尾加入一个新节点
  // 在链表末尾加入一个新节点
  function add(node, newValue) {
    if (node.next) {
      // 当前节点还不是最后一个
      add(node.next, newValue);
    } else {
      // 当前节点已经是最后一个
      var newNode = new Node(newValue);
      node.next = newNode;
    }
  }
  1. 在链表某一个节点之后加入一个新节点
  // 在链表某一个节点之后加入一个新节点
  function insert(node, insertValue, newValue) {
    if (node.value === insertValue) {
      // 当前节点之后要加入新节点
      var newNode = new Node(newValue);
      newNode.next = node.next;
      node.next = newNode;
    } else if (!node.next) {
      // 当前节点也不是,同时已经到了末尾
      var newNode = new Node(newValue);
      node.next = newNode;
    } else {
      // 当前节点不是,同时又没有到末尾,直接看下一个
      insert(node.next, insertValue, newValue);
    }
  }
  1. 删除一个链表节点
  // 删除指定值的节点,不考虑链表中有重复的值,返回删除之后的链表的首节点
  function remove(node, value) {
    // 被删除值的前一个节点指向被删除值的后一个节点 node->next
    // 处理特殊情况,只有1个节点
    if(!node) { // 无节点
      return null
    } else if (node.value === value) { 
      // 删除头
      return node.next
    } else if(node.next && node.next.value === value) {
      node.next = node.next.next
      return node
    } else {
      node.next = remove(node.next, value)
      return node
    }
  }

2. 排序和查找

排序算法

已知条件

  var nums = [3, 8, 7, 9, 6, 5];

  // 交换数组两个下标的值,很多排序算法都需要用
  function swap(nums, i, j) {
    var temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
  }
  1. 选择排序 Selection Sort
  // 选择排序
  function selectionSort(nums) {
    for (var i = 0; i < nums.length - 1; i++) {
      // 在 i ~ nums.length - 1 范围内找到最小值所在的下标
      var min = Infinity;
      var index;
      for (var j = i; j < nums.length; j++) {
        if (nums[j] < min) {
          min = nums[j];
          index = j;
        }
      }
      // 现在,找到了最小值的位置,保存到了变量index中
      // 将index 和 i 交换
      swap(nums, i, index);
    }
  }

  // 我写的选择排序
  function myselectionSort(nums){
    for(var i = 0; i < nums.length-1; i++) {
      // 初始化当前元素为此轮最小的
      var minValue = nums[i],
      minIndex = i;
      for(var j = i; j < nums.length; j++) {
        if (nums[j] < minValue) {
          minValue = nums[j]
          minIndex = j
        }
      }
      if (minIndex !== i) {
        swap(nums, i, minIndex)
      }
    }
    return nums
  }
  1. 冒泡排序 Bubble Sort
  // 冒泡排序
  function bubbleSort(nums) {
    for (var i = 0; i < nums.length - 1; i++) {
      // 依次看 0 ~ nums.length - 2 - i 范围内的数据,只要它比后面的大,就交换
      for (var j = 0; j <= nums.length - 2 - i; j++) {
        if (nums[j] > nums[j + 1]) {
          swap(nums, j, j + 1);
        }
      }
    }
    return nums
  }

  function myBubbleSort(nums){
    // 外循环共 nums.length-1次,因为每轮确定一个最大值(或最小值),只剩下一个的时候不需要了
    for(var i = 0; i < nums.length - 1; i++){
      // 每次内循环的次数为 nums.length-1-i
      // 注意,最大的值在最后面,所以每次都得从头开始
      for(var j = 0; j < nums.length-1-i; j++) {
        if (nums[j] > nums[j+1]) {
          swap(nums, j, j+1)
        }
      }
    }
    return nums
  }
  1. 快速排序 Quick Sort


选择一个数(比如序列的最后一位)作为基准数,将整个序列排序成两部分,一部分比该数小,另一部分比该数大,基准数在中间,然后对剩余的序列做同样的事情,直到排序完成
问题被转换为了:如何对数组的某个区域进行快速排序

  1. 选择4作为基准数,排序成为:(3, 2) 4 (7, 6, 5)
  2. 对于3,2, 继续使用该方式排序,得到:  (2, 3) 4 (7,6,5)
  3. 对于7,6,5,继续使用该方式排序,得到: (2, 3) 4  (5,6,7)
  4. 排序完成
  // 快速排序
  function quickSort(nums) {
    // 在指定的下标范围内,做这种骚操作(以一个数为基础,小的靠左,大的靠右)
    function _quickSort(start, end) {
      if (start >= end || start < 0 || end > nums.length - 1) {
        // 范围有问题
        return;
      }
      var low = start, // 低位游标
        high = end, // 高位游标
        key = nums[end]; // 基准值
      while (low < high) {
        //1. 低位向高位移动
        while (low < high && nums[low] <= key) {
          low++;
        }
        nums[high] = nums[low]; //把当前不合理的数字扔到高位去
        //2. 高位向低位移动
        while (low < high && nums[high] >= key) {
          high--;
        }
        nums[low] = nums[high];
      }
      // 高位低位重叠
      nums[low] = key;
      // 对左边的范围重来一次
      _quickSort(start, low - 1);
      // 对右边的范围重来一次
      _quickSort(low + 1, end);
    }

    // _quickSort(0, nums.length - 1); 
  }
  function myQuickSort(nums){
    function _quickSort(start, end) {
      if(start >= end || start < 0 || end > nums.length-1) {
        return
      }
      var low = start, 
      high=end,
      key = nums[end];
      // key是用于比较的枢纽
      while(low < high) {
        //1. 低位向高位移动
        while(low < high && nums[low] <= key){
          low++;
        }
        nums[high] = nums[low]
        //2. 高位向低位移动
        while (low < high && nums[high] >= key) {
          high--;
        }
        nums[low] = nums[high]
      }
      // 高低重叠
      nums[low] = key
      _quickSort(start, low-1)
      _quickSort(low+1, end)
    }
    _quickSort(0, nums.length-1)
  }

查询算法

  1. 顺序查找 Inorder Search
    即普通的遍历,属于算法的穷举法,没啥好解释的
  var nums = [5, 6, 7, 9, 9];

  // 顺序查找
  function inorderSearch(nums, target) {
    for (var i = 0; i < nums.length; i++) {
      if (nums[i] === target) {
        // 找到了
        return true;
      }
    }
    return false;
  }
  1. 二分查找 Binary Search
    如果一个序列是一个排序好的序列,则使用二分查找可以极大的缩短查找时间
    具体的做法是:
    查找该序列中间未知的数据
  1. 相等,找到
  2. 要找的数据较大,则对后续部分的数据做同样的步骤
  3. 要找的数据较小,则对前面部分的数据做同样的步骤
  var nums = [5, 6, 7, 9, 9];

  // 针对排序好的数组,二分查找
  function binarySearch(nums, target) {
    var minIndex = 0, // 小的下标
      maxIndex = nums.length - 1; // 大的下标
    while (minIndex <= maxIndex) {
      var mid = Math.floor((minIndex + maxIndex) / 2); // 中间下标
      if (nums[mid] === target) {
        return true; // 找到了
      } else if (nums[mid] > target) {
        maxIndex = mid - 1;
      } else {
        minIndex = mid + 1;
      }
    }
    return false;
  }
  1. 插值查找 Interpolation Search
    插值查找是对二分查找的进一步改进
    如果序列不仅是一个排序好的序列,而且序列的步长大致相同,使用插值查找会更快的找到目标。
    插值查找基于如下假设:下标之间的距离比和数据之间的距离比大致相同
    于是有
    从而有
(target - a) / (g - a) ≈ (mid - minIndex) / (maxIndex - minIndex)
mid ≈ (target - a) / (g - a) * (maxIndex - minIndex) + minIndex
  var nums = [5, 6, 7, 9, 9];

  // 针对排序好的数组,插值查找
  function interpolationSearch(nums, target) {
    var minIndex = 0, // 小的下标
      maxIndex = nums.length - 1; // 大的下标
    while (minIndex < maxIndex) {
      var mid = Math.floor(
        ((target - nums[minIndex]) / (nums[maxIndex] - nums[minIndex])) *
          (maxIndex - minIndex) +
          minIndex
      );
      // 防止左右横跳
      if (mid < minIndex || mid > maxIndex) {
        return false;
      }
      if (nums[mid] === target) {
        return true; // 找到了
      } else if (nums[mid] > target) {
        maxIndex = mid - 1;
      } else {
        minIndex = mid + 1;
      }
    }
    return false;
  }

3. 树形结构

树是一个类似于链表的二维结构,每个节点可以指向0个或多个其他节点

树具有以下特点:

树也叫 单根无环图

  1. 单根:如果一个节点A指向了另一个节点B,仅能通过A直接找到B节点,不可能通过其他节点直接找到B节点
  2. 无环:节点的指向不能形成环

树的术语:

  1. 结点的度:某个节点的度 = 该节点子节点的数量
  2. 树的度:一棵树中,最大的节点的度为该树的度
  3. 结点的层:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
  4. 树的高度或深度:树中结点的最大层次
  5. 叶子节点:度为0的结点称为叶结点;
  6. 分支节点:非叶子节点
  7. 子节点、父节点:相对概念,如果A节点有一个子节点B,则A是B的父节点,B是A的子节点
  8. 兄弟节点:如果两个节点有同一个父节点,则它们互为兄弟节点
  9. 祖先节点:某个节点的祖先节点,是从树的根到该节点本身经过的所有节点
  10. 后代节点:如果A是B的祖先节点,B则是A的后代节点

树的代码表示法:

function Node(value) {
  this.value = value; // 节点中的数据
  this.children = []; // 指向其他节点的数组
}

var a = new Node("A");
var b = new Node("B");
var c = new Node("C");
var d = new Node("D");
var e = new Node("E");
var f = new Node("F");

a.children.push(b, c);
b.children.push(d, e, f);  

二叉树

如果一颗树的度为2,则该树是二叉树

二叉树可以用下面的代码表示

function Node(value){
    this.value = value;
    this.left = null;
    this.right = null;
}

二叉树的相关算法

编写各种函数,实现下面的功能

  1. 对二叉树遍历打印

1.1 前(先)序遍历 DLR

  // 前序遍历
  function DLR(node) {
    if (!node) {
      return;
    }
    console.log(node.value); // 先自己
    DLR(node.left);
    DLR(node.right);
  }

1.2 中序遍历 LDR

  // 中序遍历
  function LDR(node) {
    if (!node) {
      return;
    }
    LDR(node.left);
    console.log(node.value);
    LDR(node.right);
  }

1.3 后序遍历 LRD

  // 后序遍历
  function LRD(node) {
    if (!node) {
      return;
    }
    LRD(node.left);
    LRD(node.right);
    console.log(node.value);
  }
  1. 根据前序遍历和中序遍历结果,得到一颗二叉树
  // 根据前序和中序遍历结果,得到一颗二叉树
  function getTree(dlr, ldr) {
    if (dlr.length === 0 && ldr.length === 0) {
      // 没有前序和中序,没有节点
      return null;
    }
    var rootValue = dlr[0]; // 通过前序遍历的第一个字符,得到根节点的value值
    var root = new Node(rootValue); // 创建根节点
    var rootIndex = ldr.indexOf(rootValue); // 根节点在中序遍历中的位置

    var leftLDR = ldr.substring(0, rootIndex); // 左边的中序遍历
    var leftDLR = dlr.substr(1, leftLDR.length); // 左边的前序

    var rightLDR = ldr.substr(rootIndex + 1); // 右边的中序
    var rightDLR = dlr.substr(leftDLR.length + 1); // 右边的前序
    root.left = getTree(leftDLR, leftLDR);
    root.right = getTree(rightDLR, rightLDR);

    return root;
  }
  1. 计算树的深度
  // 计算树的深度
  function getDeep(node) {
    if (!node) {
      return 0;
    }
    return 1 + Math.max(getDeep(node.left), getDeep(node.right));
  }
  1. 查询二叉树

    4.1 深度优先 Depth First Search

// 深度优先搜索
function deepSearch(node, target) {
  if (!node) {
    return false;
  }
  console.log(node.value);
  if (node.value === target) {
    //自己就是
    return tru e; // 找到了
  }
  return deepSearch(node.left, target) || deepSearch(node.right, target);
}

4.2 广度优先 Breadth First Search

// 广度优先搜索
function breadthSearch(node, target) {
  function _breadthSearch(nodes) {
    if (nodes.length === 0) {
      // 数组没东西
      return false;
    }
    var nextLayer = []; // 下一层的节点
    for (var i = 0; i < nodes.length; i++) {
      console.log(nodes[i].value);
      if (nodes[i].value === target) {
        return true;
      }
      if (nodes[i].left) {
        nextLayer.push(nodes[i].left);
      }
      if (nodes[i].right) {
        nextLayer.push(nodes[i].right);
      }
    }
    return _breadthSearch(nextLayer);
  }

  return _breadthSearch([node]);
}
  1. 比较两棵二叉树,得到它们的差异
  // 对比两个树的差异,返回差异结果(数组)
  function diff(node1, node2) {
    var result = [];
    if (!node1 && !node2) {
      return result; // 两个节点都没有值,不可能有差异,返回空数组即可
    }
    if (!node1 && node2) {
      result.push({ type: "新增", from: null, to: node2 });
      return result; // 这种情况不需要继续往后比较了
    }
    if (node1 && !node2) {
      result.push({ type: "删除", from: node1, to: null });
      return result; // 这种情况不需要继续往后比较了
    }
    if (node1.value !== node2.value) {
      result.push({ type: "修改", from: node1, to: node2 }); // 加入「修改」的变化
      // 不能停止
    }
    var leftDiff = diff(node1.left, node2.left); // 左树的差异
    var rightDiff = diff(node1.right, node2.right); // 右树的差异
    
		// return [...result, ...leftDiff, ...rightDiff]
    return result.concat(leftDiff).concat(rightDiff); // 合并差异
  }

  var node1 = getTree("ABDGECF", "GDBEAFC");
  var node2 = getTree("AKDECFT", "DKEAFCT");

  var result = diff(node1, node2);
  console.log(result);

4. 图结构

图的概念

图结构中,一个结点可以链接到任意结点,所有结点链接而成的结构,即为图结构

图结构中的链接可以是有向的,也可以是无向的(双向链接),本文仅讨论双向链接

树结构是一种特殊的图结构

图结构没有根,可以有环,但是在一个图结构中,不能存在两个或以上的孤立结点

可以使用图中任何一个结点表示整个图结构

图结构是一种常见的数据结构,例如网络爬虫抓取的网页就是一种典型的图结构

图结构的代码可表示为:

function Node(value){
    this.value = value;
    this.neighbors = [];
}

相关算法

查询算法

  1. 和树结构一样,图结构的查询也可以分为深度优先(Depth First Search)和广度优先(Breadth First Search)查询

深度优先

function Node(value) {
  this.value = value;
  this.neighbors = [];
}

// 图搜:深度优先
function deepSearch(node, target) {
  var found = []; // 已经找过的节点
  function _deepSearch(node) {
    // 该函数内部无论进行多少次递归,使用的都是同一个found变量
    if (found.includes(node)) {
      // 如果该节点已经找过
      return false;
    }
    if (node.value === target) {
      return true; // 找到了
    }
    found.push(node); // 这个节点不是,已经找过了
    for (var i = 0; i < node.neighbors.length; i++) {
      var nextNode = node.neighbors[i];
      if (_deepSearch(nextNode)) {
        // 通过邻居找到了
        return true;
      }
    }
    return false; // 所有的邻居都不是
  }

  return _deepSearch(node);
}

广度优先

  // 图搜:广度优先
  function breadthSearch(node, target) {
    var found = []; // 已经找过的
    // 必须保证:nodes参数中的节点都是没有找过的
    function _breadthSearch(nodes) {
      if (nodes.length === 0) {
        // 这一层已经没有任何节点了
        return false;
      }
      var next = []; // 下一层的节点数组
      for (var i = 0; i < nodes.length; i++) {
        var n = nodes[i]; //当前找的节点是n
        if (n.value === target) {
          return true;
        }
        found.push(n); //这个节点不是,加入到已找节点中
        // n不是,得把n的邻居加入下一层的节点数组中
        for (var j = 0; j < n.neighbors.length; j++) {
          if (!next.includes(n.neighbors[j])) {
            // 该判断是为了保证next数组中没有重复的
            next.push(n.neighbors[j]); // 把n的这个邻居加入到下一层
          }
        }
      }

      // 这一层已经看完了,found已经更新了
      // 此时,再来去掉next中已经找过的节点
      for (var i = 0; i < next.length; i++) {
        if (found.includes(next[i])) {
          next.splice(i, 1);
          i--;
        }
      }
      return _breadthSearch(next);
    }

    return _breadthSearch([node]);
  }

最小生成树算法

  1. 如果一个图中结点连接而成的边具备某种数值,需要将这些边进行精简,生成一个连接全节点同时总边长最小的树结构,该树称之为最小生成树
    实现最小生成树可以使用Prim算法,从任意一个点出发,连接到该点最短的点,组成一个部落,然后继续连接到该部落最短的点,直到把所有点连接完成
  var nodes = [
    new Node("a"),
    new Node("b"),
    new Node("c"),
    new Node("d"),
    new Node("e"),
  ]; // 节点的数组

  var sides = [
    [0, 8, 3, Infinity, 4],
    [8, 0, 4, 10, Infinity],
    [3, 4, 0, 3, Infinity],
    [Infinity, 10, 3, 0, 6],
    [4, Infinity, Infinity, 6, 0],
  ];

  // 根据边的情况,将nodes中的节点连起来
  function Prim(nodes, sides) {
    if (nodes.length <= 1) {
      return;
    }
    var hord = [nodes[0]]; // 部落(部落中的每一个村庄是连通的),初始只有一个村庄
    while (hord.length < nodes.length) {
      // 说明还有村庄没有加入到部落
      // 继续向部落中添加村庄
      connectToHord();
    }

    // 从非部落节点中,找到一个最短距离的节点,连接到部落
    function connectToHord() {
      var result = {
        from: null,
        to: null,
        dis: Infinity,
      };
      for (var i = 0; i < nodes.length; i++) {
        var n = nodes[i];
        if (!hord.includes(n)) {
          // 不在部落中的节点
          var info = getMinDistance(n); // 看一下这个点到部落的情况
          if (info.dis < result.dis) {
            // 发现了有更短的节点
            result = info;
          }
        }
      }
      // result一定是最优的点
      result.from.neighbors.push(result.to);
      result.to.neighbors.push(result.from);
      // 加入部落
      hord.push(result.from);
    }

    // 得到node到部落最小的距离,以及连接到部落的哪个点
    function getMinDistance(node) {
      var result = {
        from: node,
        to: null,
        dis: Infinity,
      };
      for (var i = 0; i < hord.length; i++) {
        var dis = getDistance(node, hord[i]);
        if (dis < result.dis) {
          // 有更小的距离
          result.to = hord[i];
          result.dis = dis;
        }
      }
      return result;
    }
    // 得到两个节点之间的距离
    function getDistance(node1, node2) {
      if (node1 === node2) {
        // 不能得到自己到自己的距离
        throw new Error("不能得到自己到自己的距离");
      }
      var row = nodes.indexOf(node1);
      var col = nodes.indexOf(node2);
      return sides[row][col];
    }
  }

  Prim(nodes, sides);

5. 贪心算法和动态规划

贪心算法

面试题:找零问题

示例:假设你有一间小店,需要找给客户46分钱的硬币,你的货柜里只有面额为25分、10分、5分、1分的硬币,如何找零才能保证数额正确并且硬币数最小

贪心算法:当遇到一个求解全局最优解问题时,如果可以将全局问题切分为小的局部问题,并寻求局部最优解,局部最优解累计的结果即全局最优解

某些情况下,局部最优解累计的结果并不一定是全局最优解

// 找零问题
// total:找零总数
// denos:目前的面额
function exchange(total, denos) {
  var result = []; // 存放找零结果的数组
  while (total > 0) {
    // 还有钱需要找,得继续找零
    // 找出面额中最大的能找的面额
    var max = -Infinity;
    for (var i = 0; i < denos.length; i++) {
      if (denos[i] > max && denos[i] <= total) {
        max = denos[i];
      }
    }
    result.push(max); // 结果加入该面额
    total -= max; // 找零总数减少
  }
  return result;
}

var result = exchange(76, [50, 35, 10, 5, 1]);
console.log(result);
// 输出
[50, 10, 10, 5, 1]
现实生活中没有这种面额
但是此时最优解为:[35, 35, 5, 1]

动态规划

动态规划:一个求全局最优解的问题,把它分解为多个重复子问题,对重复子问题求解,同时缓存那些重复子过程

面试题1:青蛙跳台阶问题

有N级台阶,一只青蛙每次可以跳1级或两级,一共有多少种跳法可以跳完台阶?

方法1

  var count = 0;

  function jump(n) {
    var cache = []; // 缓存n对应的结果  cache[n] = 结果
    function _jump(n) {
      if (n === 1) {
        return 1;
      }
      if (n === 2) {
        return 2;
      }
      //看缓存中有没有记录
      if (cache[n]) {
        return cache[n]; // 有缓存,直接返回
      }

      count++; // 测试,计算次数+1
      console.log("当前在正运算的n的值:", n);
      var result = _jump(n - 1) + _jump(n - 2);
      cache[n] = result; // 把结果缓存起来,以供将来使用
      return result;
    }
    var r = _jump(n);
    console.log(cache);
    return r;
  }

方法2

function jump(n) {
  first = 1
  second = 2
  third = 0
  if (n === 1) {
    return first;
  }
  if (n === 2) {
    return second;
  }
  for(var i = 0; i < n-2; i++) {
    third = second + first
    first = second
    second = third
  }
  return third
}

面试题2:最长公共子序列问题(LCS)

有的时候,我们需要比较两个字符串的相似程度,通常就是比较两个字符串有多少相同的公共子序列

公共子序列不要求连续

例如有两个字符串

  • LCS算法可以很好的计算出两篇论文的相似度
  • 利用LCS,可以计算出搜索关键字与搜索结果的相似度

以上两个字符串的最长公共子序列为:LCS可以计算出的相似度

// 得到两个字符串的最长公共子序列
function LCS(str1, str2) {
  var cache = {}; // i-j: 结果
  // i: 指向第一个字符串中的某个位置
  // j: 指向第二个字符串中的某个位置
  function _lcs(i, j) {
    // 看这两个位置如何处理
    if (i >= str1.length || j >= str2.length) {
      // i或j已经超出了范围
      return "";
    }
    var cacheKey = i + "-" + j; //缓存的属性名字
    if (cache[cacheKey]) {
      // 有缓存
      return cache[cacheKey];
    }
    var result; // 计算结果
    if (str1[i] === str2[j]) {
      // 当前这个字符,一定进入最长公共子序列
      result = str1[i] + _lcs(i + 1, j + 1);
    } else {
      // i不动
      var lcs1 = _lcs(i, j + 1);
      // j不动
      var lcs2 = _lcs(i + 1, j);
      result = lcs1.length > lcs2.length ? lcs1 : lcs2;
    }
    //缓存结果
    cache[cacheKey] = result;
    return result;
  }

  return _lcs(0, 0);
}

var result = LCS(
  "LCS算法可以很好的计算出两篇论文的相似度",
  "利用LCS,可以计算出搜索关键字与搜索结果的相似度"
);
console.log(result);