算法入门学习

一、线性结构 {ignore}

数据结构和算法概述

  1. 什么是数据结构?

存储和运算是程序的两大基础功能,数据结构是专门研究数据存储的学科。

很多时候,我们无法仅使用简单的数字、字符串、布尔就能完整的描述数据,可能我们希望使用数组、对象、或它们组合而成的复合结构来对数据进行描述。这种复合的结构就是数据结构。

而在实际开发中,我们会发现很多场景中使用的数据结构有着相似的特征,于是,数据结构这门学科,就把这些相似的结构单独提取出来进行研究。

在这门学科中,常见的数据结构有:数组、链表、树、图等

  1. 什么是算法?

存储和运算是程序的两大基础功能,算法是专门研究运算过程的学科。

一个程序,很多时候都需要根据一种已知数据,通过计算,得到另一个未知数据,这个运算过程使用的方法,就是算法。

而在很多的场景中,它们使用的算法有一些共通的特点,于是把这些共通的算法抽象出来,就行了常见算法。

从一个更高的角度来对算法划分,常见的算法有:穷举法、分治法、贪心算法、动态规划

  1. 数据结构和算法有什么关系?

一个面向的是存储,一个面向的是运算,它们共同构成了计算机程序的两个重要部分。

有了相应的数据结构,免不了对这种数据结构的各种变化进行运算,所以,很多时候,某种数据结构都会自然而然的搭配不少算法。

  1. 数据结构和算法课程使用什么计算机语言?

数据结构和算法属于计算机基础课程,它们和具体的语言无关,用任何语言都可以实现。

本课程采用JavaScript语言。

线性结构

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

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

数组

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

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

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

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

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

JS中的数组

在ES6之前,JS没有真正意义的数组,所谓的Array,实际上底层实现是链表。

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

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

链表

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

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

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

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

手动用代码实现链表

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

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

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

  1. 遍历打印
  2. 获取链表的长度
  3. 通过下标获取链表中的某个数据
  4. 通过下标设置链表中的某个数据
  5. 在链表某一个节点之后加入一个新节点
  6. 在链表末尾加入一个新节点
  7. 删除一个链表节点
  8. 链表倒序
/**
 * 构造函数,表示链表的一个节点
 */
function Node(value) {
    this.value = value; //节点的数据
    this.next = null; //下一个节点的地址
}

/**
 * 遍历一个链表,打印每个节点的数据
 * @param root 链表的根节点
 */
function print(root) {
    // var node = root;
    // while (node) {
    //     //如果node有值,打印
    //     console.log(node.value);
    //     node = node.next;
    // }

    // 分治法
    if (root) {
        console.log(root.value); //打印自己
        print(root.next);
    }
}

/**
 * 计算链表的长度
 * @param {*} root 
 */
function count(root) {
    if (!root) return 0; //链表没有节点
    return 1 + count(root.next); //1表示根节点占用一个数量
}

/**
 * 得到链表某个下标的数据
 * @param {*} root 
 * @param {*} index 
 */
function getNode(root, index) {
    /**
     * 判断某个节点是否是我要查找的节点
     * @param {*} node 表示某个节点
     * @param {*} i 该节点是第几个节点
     */
    function _getValue(node, i) {
        if (!node) return null;
        if (i === index) return node;
        return _getValue(node.next, i + 1);
    }
    return _getValue(root, 0);
}

/**
 * 设置链表某个位置的数据
 */
function setValue(root, index, value) {
    function _setValue(node, i) {
        if (!node) return;
        if (i === index) {
            node.value = value
        }
        else {
            _setValue(node.next, i + 1);
        }
    }
    _setValue(root, 0);
}

/**
 * 在某个链表节点之后加入一个新节点
 * @param node 在哪个节点之后加入
 * @param newValue 新节点的数据
 */
function insertAfter(node, newValue) {
    var newNode = new Node(newValue); //构建新节点

    node.next = newNode;
    newNode.next = node.next;
}

/**
 * 在链表的末尾加入新节点
 */
function push(root, newValue) {
    //判断root是不是最后一个节点
    if (!root.next) {
        //最后一个节点
        var newNode = new Node(newValue);
        root.next = newNode;
    }
    else {
        push(root.next, newValue); //自己不是最后一个,看下一个
    }
}

/**
 * 根据给定的链表,和 给定的要删除的值,删除对应节点
 * @param {*} root 
 * @param {*} nodeValue 
 */
function removeNode(root, nodeValue) {
    if (!root || !root.next) return; //无法删除的情况
    if (root.next.value === nodeValue) {
        //下一个节点就是要找的节点
        root.next = root.next.next;
    }
    else {
        //下一个节点还不是
        removeNode(root.next, nodeValue);
    }
}

/**
 * 给定一个链表,返回一个倒序后的根节点
 * @param {*} root 
 */
function reverse(root) {
    if (!root || !root.next) return root;
    if (!root.next.next) {
        var temp = root.next; //保存返回的节点
        //有两个节点的链表
        root.next.next = root;
        root.next = null;
        return temp;
    }
    else {
        var temp = reverse(root.next); //后续节点倒序
        root.next.next = root;
        root.next = null;
        return temp;
    }
}

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

a.next = b;
b.next = c;

// insertAfter(b, "d");

var temp = reverse(a);
print(temp);

二、排序和查找 {ignore}

排序算法

排序算法没有优劣之分,在不同的场景中,不同的排序算法执行效率不同。

  1. 选择排序 Selection Sort

一次选择排序,可以将某个区间的最小值排列到该区域的第一位,具体的方式是:

  1. 找出该区域的最小值
  2. 将该值与该区域第一个值交换
  3. 对下一个区域重复上述过程,直到排序完成
  1. 冒泡排序 Bubble Sort

一次冒泡排序,可以将某个区域序列的最大值排序到该区域的最后一位,具体的方式是:

  1. 将第1位和第2位比较,如果前者比后者大则交换
  2. 将第2位和第3位比较,如果前者比后者大则交换
  3. 依次类推,直到比较到该区域的最后两位
  4. 重复上述过程,直到序列排序完成
  1. 插入排序 Insertion Sort

将序列分为两个部分,一部分是有序的,一部分是无序的,现在要做的是,就是不断的从无序的部分取出数据,加入到有序的部分,直到整个排序完成

例如:序列[5, 7, 2, 3, 6]

  1. 分为有序的序列和无序的序列 (5) (7 2 3 6)
  2. 不断的扩充有序序列 (5 7) (2 3 6)
  3. 不断的扩充有序序列 (2 5 7) (3 6)
  4. 不断的扩充有序序列 (2 3 5 7) (6)
  5. 不断的扩充有序序列 (2 3 5 6 7)
  6. 排序完成
  1. 快速排序 Quick Sort

选择一个数(比如序列的最后一位)作为基准数,将整个序列排序成两部分,一部分比该数小,另一部分比该数大,基准数在中间,然后对剩余的序列做同样的事情,直到排序完成

例如:序列[5, 7, 2, 3, 6, 4]

  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. 排序完成

查询算法

  1. 顺序查找 Inorder Search

即普通的遍历,属于算法的穷举法,没啥好解释的

  1. 二分查找 Binary Search

如果一个序列是一个排序好的序列,则使用二分查找可以极大的缩短查找时间

具体的做法是:

查找该序列中间未知的数据

  1. 相等,找到
  2. 要找的数据较大,则对后续部分的数据做同样的步骤
  3. 要找的数据较小,则对前面部分的数据做同样的步骤
  1. 插值查找 Interpolation Search

插值查找是对二分查找的进一步改进

如果序列不仅是一个排序好的序列,而且序列的步长大致相同,使用插值查找会更快的找到目标。

插值查找基于如下假设:下标之间的距离比和数据之间的距离比大致相同,即:

(目标下标-最小下标) / (最大下标 - 最小下标) ≈ (目标值 - 最小值) / (最大值 - 最小值)

因此可以算出大致的下标落点:

目标下标 ≈ (目标值 - 最小值) / (最大值 - 最小值) * (最大下标 - 最小下标) + 最小下标

这样就可以计算出大致的下标落点,后续的比较和二分查找一样。

// sort.js
/**
 * 交换数组中指定的位置
 * @param {*} arr 
 * @param {*} i1 
 * @param {*} i2 
 */
function swap(arr, i1, i2) {
    var temp = arr[i1]
    arr[i1] = arr[i2];
    arr[i2] = temp;
}

/**
 * 选择排序
 * @param {*} arr 
 */
function selectionSort(arr) {
    for (var i = 0; i < arr.length - 1; i++) {
        //搞定 i ~ arr.length-1 区间
        //从该区间中找出最小值,和 第 i 位交换
        var min = arr[i]; //定义一个变量,为该区间的第一个数
        var index = i; //最小值所在的位置
        for (var j = i + 1; j < arr.length; j++) {
            if (arr[j] < min) {
                min = arr[j];
                index = j; //重新记录最小值的位置
            }
        }
        //最小值已经找出
        //交换第i位和第index位
        swap(arr, i, index);
    }
}

/**
 * 冒泡排序
 * @param {*} arr 
 */
function bubbleSort(arr) {
    for (var i = 0; i < arr.length - 1; i++) {
        //需要经过arr.length-1次的冒泡
        //i:0   循环:0~arr.length-1-i
        //i:1   循环:0~arr.length-1-i
        //i:2   循环: 0~arr.length-1-i
        for (var j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
            }
        }
    }
}

/**
 * 插入排序
 * @param {*} arr 
 */
function insertionSort(arr) {
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] < arr[i - 1]) {
            //将第i位的值加入到前面有序队列的正确位置
            var temp = arr[i];
            for (var j = i; j >= 0; j--) {
                if (j > 0 && arr[j - 1] > temp) {
                    arr[j] = arr[j - 1];
                }
                else {
                    arr[j] = temp;
                    break;
                }
            }
        }
    }
}

/**
 * 快速排序
 * @param {*} arr 
 */
function quickSort(arr) {
    /**
     * 对数组的某个区域进行一次快速排序
     * @param {*} arr 
     * @param {*} start 区域的起始下标
     * @param {*} end 区域的结束下标
     */
    function _quickSort(arr, start, end) {
        if (start >= end || start > arr.length - 1) return;
        var low = start, high = end;
        var key = arr[end]; //基准值
        while (low < high) {
            while (low < high && arr[low] <= key) low++;
            arr[high] = arr[low];
            while (low < high && arr[high] >= key) high--;
            arr[low] = arr[high];
        }
        //low === high
        arr[low] = key;
        _quickSort(arr, start, low - 1);
        _quickSort(arr, low + 1, end);
    }
    _quickSort(arr, 0, arr.length - 1);
}

var arr = [5, 3, 1, 6, 7, 4];
console.log(arr)
quickSort(arr)
console.log(arr);
//search.js
var num = 0;

/**
 * 查找一个数组中目标值是否存在(顺序查找)
 * @param {*} arr 
 * @param {*} target 
 */
function inOrderSearch(arr, target) {
    for (var i = 0; i < arr.length; i++) {
        num++;
        if (arr[i] === target) {
            return true;
        }
    }
    return false;
}

function binarySearch(arr, target) {
    if (arr.length === 0 || target < arr[0] || target > arr[arr.length - 1]) return false;

    var minIndex = 0; //最小下标
    var maxIndex = arr.length - 1; //最大下标
    while (minIndex <= maxIndex) {
        num++;
        var mid = Math.floor((minIndex + maxIndex) / 2);//中间下标
        if (arr[mid] === target) {
            return true;
        }
        else if (arr[mid] > target) {
            maxIndex = mid - 1;
        }
        else {
            minIndex = mid + 1;
        }
    }
    return false;
}

function interpolationSearch(arr, target) {
    if (arr.length === 0 || target < arr[0] || target > arr[arr.length - 1]) return false;
    var minIndex = 0; //最小下标
    var maxIndex = arr.length - 1; //最大下标
    while (minIndex <= maxIndex) {
        num++;
        var mid = (target - arr[minIndex]) / (arr[maxIndex] - arr[minIndex]) * (maxIndex - minIndex) + minIndex;
        if (arr[mid] === target) {
            return true;
        }
        else if (arr[mid] > target) {
            maxIndex = mid - 1;
        }
        else {
            minIndex = mid + 1;
        }
    }
    return false;
}

var arr = new Array(100000);
for (var i = 0; i < arr.length; i++) {
    arr[i] = i + 1;
}

var result = interpolationSearch(arr, 100000)
console.log(result, num);

三、树形结构 {ignore}

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

2019-11-22-16-21-49.png

树具有以下特点:

  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 = [];
}

二叉树

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

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

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

二叉树的相关算法

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

  1. 对二叉树遍历打印
    1. 前(先)序遍历 DLR
    2. 中序遍历 LDR
    3. 后序遍历 LRD
  2. 根据前序遍历和中序遍历结果,得到一颗二叉树
  3. 计算树的深度
  4. 查询二叉树
    1. 深度优先 Depth First Search
    2. 广度优先 Breadth First Search
  5. 比较两棵二叉树,得到比较的结果
function Node(value) {
    this.value = value;
    this.left = null;
    this.right = null;
}

/**
 * 前序遍历
 * @param {*} root 
 */
function DLR(root) {
    if (!root) return;// 没有节点
    console.log(root.value);
    DLR(root.left);
    DLR(root.right);
}

/**
 * 中序遍历
 * @param {*} root 
 */
function LDR(root) {
    if (!root) return;// 没有节点
    LDR(root.left);
    console.log(root.value);
    LDR(root.right);
}

/**
 * 后序遍历
 * @param {*} root 
 */
function LRD(root) {
    if (!root) return;// 没有节点
    LRD(root.left);
    LRD(root.right);
    console.log(root.value);
}

/**
 * 根据前序遍历,和 中序遍历,得到一棵树的根节点
 * @param {*} dlr 
 * @param {*} ldr 
 */
function getTree(dlr, ldr) {
    dlr = dlr.split("");
    ldr = ldr.split("");
    if (dlr.length !== ldr.length) throw new Error("无效的遍历值");
    if (dlr.length === 0) return null;

    var rootValue = dlr[0]; //取出根节点的值 
    var root = new Node(rootValue);

    var index = ldr.indexOf(rootValue); //找到根节点的值在中序遍历中的位置
    var leftLDR = ldr.slice(0, index).join(""); //左边的中序遍历结果
    var leftDLR = dlr.slice(1, leftLDR.length + 1).join(""); //左边的前序遍历结果
    root.left = getTree(leftDLR, leftLDR);

    var rightLDR = ldr.slice(index + 1).join(""); //右边的中序遍历结果
    var rightDLR = dlr.slice(leftLDR.length + 1).join(""); //右边的前序遍历结果
    root.right = getTree(rightDLR, rightLDR);

    return root;
}

/**
 * 得到一棵树的深度
 * @param {*} root 
 */
function getDeep(root) {
    if (!root) return 0;
    var left = getDeep(root.left);
    var right = getDeep(root.right);
    return Math.max(left, right) + 1;
}

var root = getTree("abcde", "cbdae")
console.log(root)
console.log(getDeep(root));

// 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.left = b;
// a.right = e;

// b.left = c;
// b.right = d;

// e.left = f;

// LRD(a);

四、图结构 {ignore}

概念

2019-11-24-14-17-49.png

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

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

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

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

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

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

图结构的代码可表示为:

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

相关算法

  1. 查询算法

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

  1. 最小生成树算法

如果一个图中结点连接而成的边具备某种数值,需要将这些边进行精简,生成一个连接全节点同时总边长最小的树结构,该树称之为最小生成树

实现最小生成树可以使用Prim算法,从任意一个点出发,连接到该点最短的点,组成一个部落,然后继续连接到该部落最短的点,直到把所有点连接完成

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

/**
 * 深度优先遍历
 * @param {*} node 
 * @param {*} targetValue 
 * @param finded 已经找过的结点
 */
function deepFirstSearch(node, targetValue, finded) {
    //如果finded数组中包含了node,说明,当前结点已经被看过了,直接返回
    if (finded.includes(node)) return false;
    if (node.value === targetValue) return true;
    finded.push(node); //将当前结点加入到已找过的结点
    //自己不是要找到,看相邻结点
    for (var i = 0; i < node.neighbors.length; i++) {
        if (deepFirstSearch(node.neighbors[i], targetValue, finded)) {
            //在其中一个相邻结点的深搜过程中找到了
            return true;
        }
    }
    return false; //所有相邻的结点的深搜过程中都找不到
}

/**
 * 图的广搜
 * @param {*} nodes 要找的某一群结点,该数组中的结点都是没有找过的
 * @param {*} targetValue 
 * @param finded 已经找过的结点
 */
function breadthFirstSearch(nodes, targetValue, finded) {
    if (nodes.length === 0) return false;
    var nexts = [];
    for (var i = 0; i < nodes.length; i++) {
        if (nodes[i].value === targetValue) {
            return true;
        }
        //说明该结点已找过
        finded.push(nodes[i]);
        //直接将该结点的相邻结点加入到数组nexts
        for (var j = 0; j < nodes[i].neighbors.length; j++) {
            var n = nodes[i].neighbors[j]; //第j个邻居
            if (!nexts.includes(n)) {
                nexts.push(n);
            }
        }
    }
    //重新对nexts进行处理
    for (var i = 0; i < nexts.length; i++) {
        if (finded.includes(nexts[i])) {
            nexts.splice(i, 1);
            i--;
        }
    }
   
    console.log(nexts);
    return breadthFirstSearch(nexts, targetValue, finded);
}

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");

a.neighbors.push(b, c, e);
b.neighbors.push(a, c, d);
c.neighbors.push(a, b);
e.neighbors.push(a, e);
d.neighbors.push(b, e);

var result = breadthFirstSearch([c], "e", []);
console.log(result);

/**
 * 使用Prim算法根据点的集合,和边的集合,链接结点
 * @param {*} nodes 
 * @param {*} sides 
 */
function Prim(nodes, sides) {
    //先做一个验证
    if (nodes.length !== sides.length || nodes.length <= 1) return;

    var horde = [nodes[0]]; //已连接的点形成的部落

    while (horde.length < nodes.length) {
        //添加一个点到部落horde
        //找到一个到这个部落最短的点
        var result = { //求的最短的点
            dis: Infinity, //距离默认无穷大
            to: null, //连接到部落的哪个点,部落的点
            from: null //从哪个点连接到部落,新的点
        }
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i]; //一个点一个点拿出来比较
            if (horde.includes(node)) {
                //部落中已经有这个点了
                continue; //进行下一次循环
            }
            //这个点没有在部落中
            var info = getMinDistance(node); //得到该点到部落的距离信息
            if (info.dis < result.dis) {
                result.dis = info.dis;
                result.to = info.connect;
                result.from = node;
            }
        }
        //将点和部落中对应的点进行连接
        result.to.neighbors.push(result.from);
        result.from.neighbors.push(result.to);
        //将该点加入到部落中
        horde.push(result.from);
    }

    /**
     * 查找指定的结点到当前部落最短的距离和要连接的点
     * @param {*} node 
     */
    function getMinDistance(node) {
        var result = { //求的结果
            dis: Infinity, //距离默认无穷大
            connect: null //连接到部落的哪个点
        }
        for (var i = 0; i < horde.length; i++) {
            var target = horde[i]; //部落中的某个结点
            var dis = getDistance(node, target);
            if (dis < result.dis) {
                result.dis = dis;
                result.connect = target;
            }
        }
        return result;
    }

    /**
     * 得到两个点的距离
     * @param {*} node1 
     * @param {*} node2 
     */
    function getDistance(node1, node2) {
        var i1 = nodes.indexOf(node1); //第一个点的下标
        var i2 = nodes.indexOf(node2);//第二个点的下标
        return sides[i1][i2];
    }
}


//点的集合
var nodes = [a, b, c, d, e];
//边的集合
var sides = [
    [0, 8, 3, Infinity, 4],//a点到其他结点的距离
    [8, 0, 4, 10, Infinity], //b到其他点的距离
    [3, 4, 0, 3, Infinity], //c到其他点的距离
    [Infinity, 10, 3, 0, 6], //d到其他点的距离
    [4, Infinity, Infinity, 6, 0], //e到其他点的距离
];

Prim(nodes, sides);
console.log(a)

五、贪心算法和动态规划 {ignore}

贪心算法

当遇到一个求解全局最优解问题时,如果可以将全局问题切分为小的局部问题,并寻求局部最优解,同时可以证明局部最优解累计的结果就是全局最优解,则可以使用贪心算法

面试题:找零问题

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

动态规划

分治法有一个问题,就是容易重复计算已经计算过的值,使用动态规划,可以讲每一次分治时算出的值记录下来,防止重复计算,从而提高效率。

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

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

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

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

例如有两个字符串

  • 邓哥特有贵族气质吸引了很多
  • 邓哥喜欢吃秋葵和香菜,但是他的女朋友们不喜欢

以上两个字符串的最长公共子序列为:邓哥的女

/**
 * 贪心算法:找零问题
 * @param {*} total 找零总数
 * @param {*} deno 面额
 */
function exchange(total, deno) {
    var result = []; //找零结果
    while (total > 0) {
        //还要找
        var max = -1; //最大可用面额
        for (var i = 0; i < deno.length; i++) {
            var d = deno[i]; //当前面额
            if (d > max && d <= total) {
                max = d;
            }
        }
        result.push(max); //找钱结果
        total -= max;
    }
    return result;
}

var result = exchange(41, [25, 20, 10, 5, 1]) 
console.log(result);

-------------------------------------------------------
function jumpTemp(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    return jumpTemp(n - 1) + jumpTemp(n - 2);
}

/**
 * 青蛙跳n级台阶一共有多少种跳法
 * @param {*} n 
 */
function jump(n) {
    var table = []; //用一个数组记录已经跳过的台阶结果

    function _jump(n) {
        if (table[n]) return table[n]; //已经算过了,不用再算了
        //没有算过
        var newRecord; //用于记录这一次运算的结果
        if (n === 1) newRecord = 1;
        else if (n === 2) newRecord = 2;
        else {
            newRecord = _jump(n - 1) + _jump(n - 2);
        }
        table[n] = newRecord;
        return newRecord;
    }

    var result = _jump(n);
    console.log(table);
    return result;
}

/**
 * 
 * @param {*} str1 
 * @param {*} str2 
 */
function LCS(str1, str2) {
    var table = [];

    function _LCS(str1, str2) {
        //判断目前的输入值是否有对应的计算结果(是不是已经存过了)
        for (var i = 0; i < table.length; i++) {
            if (table[i].str1 === str1 && table[i].str2 === str2) {
                return table[i].result;
            }
        }
        //没有存储结果
        var newResult; //用于计算最终计算的结果
        if (!str1 || !str2) newResult = "";// 其中有一个字符串没东西
        else if (str1[0] === str2[0]) {
            //开头相同
            newResult = str1[0] + _LCS(str1.substr(1), str2.substr(1));
        }
        else {
            var s1 = _LCS(str1, str2.substr(1));
            var s2 = _LCS(str1.substr(1), str2);
            if (s1.length < s2.length) {
                newResult = s2;
            }
            else {
                newResult = s1;
            }
        }
        table.push({
            str1: str1,
            str2: str2,
            result: newResult
        })
        return newResult;
    }

    var result = _LCS(str1, str2);
    console.log(table)
    return result;

}

var result = LCS("邓哥特有的贵族气质吸引了很多女孩", "邓哥喜欢吃秋葵和香菜,但是他的女朋友们不喜欢");
console.log(result);

----------------------------------思考题-------------------------------------
/**
 * 
 * @param {*} total 找零总数 41
 * @param {*} denos 数组,记录面额 [25, 20, 5, 1]
 * @returns 数组,得到最优解 例如:[20, 20, 1],如果无解,返回false
 */
function 找零问题(total, denos) {
    // 如果 denos数组长度为0,无解
    // 把denos第一个面额拿出来,看看该面额是否和total相等,如果相等,直接放到最优解中
    // 第一个面额比total大,这个面额不能找零,重新看剩下面额(分治)
    // 第一个面额比total小,两种情况
    // 1. 如果我找了这个面额的最优解
    // 2. 我没有找这个面额的最优解
    // 根据上面两种情况看哪种情况最优,取最优的
}

你可能感兴趣的:(算法入门学习)