1

初衷

想要学习算法大概是感觉到面对复杂业务,以及想阅读源码时,感到力不从心。源码中贯穿着优秀的算法思想,一个优雅的实现,在我看来需要想破脑袋才能理解,而如果有了算法理解,其实是自然而然的事情。所以决定学一学算法。

算法图解

在这里,向大家推荐一本书,算法图解。正如这本书副标题所写:像小说一样有趣的算法入门书。整本书讲解算法之前 通常从实际应用中引出问题,像探案一样一步一步道出 真谛。书中绝无长篇大论以及枯燥的公式,书中穿插了400多个示意图,生动的介绍算法执行过程,可以说对算法初学者简直太友好了。
好了,下面介绍一下算法图解每章节的内容,以及js实现。
注意: 下面分章节介绍,是在也算不上介绍,更多的是js实现,对于算法思想和真正的细节,还是希望大家读原书。我顶多算抛砖引玉,轻喷

二分查找

二分查找用于有序列表的查找,一个简单的猜大小的例子:
我随便想一个 1-100的数字,你的目标是以最小的次数猜到这个数字,你每次猜测后,我会说 小了、大了、或者对了。
最快的做法是:

  1. 猜50。
  2. 小了,但是排除了一半的数字,然后猜 75
  3. 大了,那余下的数字又排除了一半!然后猜63
  4. 大了,那就猜 57
  5. 大就猜 53, 小则猜 60
  6. 。。。 总之 范围在一步步的减小。最多7步肯定能猜对。

这就是二分查找了。
二分查找的实现,有2种,一种是使用while循环,一种是使用递归。我把两种实现都写出来。

// while
function binary_search1(list, key) {
    let low = 0;
    let high =  list.length - 1;
    let mid = -1;
    while(low <= high) {
        mid = parseInt(low + (high - low) /2);
        if (key === list[mid]) {
            return mid;
        } else if (key > list[mid]) {
            low = mid + 1;
        } else if (key < list[mid]) {
            high = mid - 1;
        } else {
            return -1;
        }
    }
    return -1;
};
// recursive 
function binary_search2(list, low, high, key) {
    if (low > high) {
        return -1;
    }
    let mid = parseInt((low + (high - low)/2));
    if (key === list[mid]) {
        return mid;
    }
    if (key > list[mid]) {
        low = mid + 1;
        return binary_search2(list, low, high, key)
    } 
    if (key < list[mid]) {
        high = mid - 1;
        return binary_search2(list, low, high, key);
    }
}

选择排序

第二章讲了两种基本数据结构: 数组和链表。然后在此基础上 讲解了选择排序。
选择排序思想是,遍历列表,找出最大的元素放到另一个列表中。再次这样做,找出第二大元素放到另一个列表中。
实现可参考:
ps. 排序类可以用leetcode#912验证

function selectSort(arr) {
    const res = [];
    while (arr.length) {
        res.push(...arr.splice(getSmallest(arr), 1))
    }
    return res;
}
function getSmallest(arr) {
    let smallestValue = Infinity;
    let smallestIndex = -1;
    arr.forEach((item, index) => {
        if (item <= smallestValue) {
            smallestValue = item;
            smallestIndex = index;
        }
    });
    return smallestIndex;
}

递归

第三章讲解了 递归算法。递归算法我们大抵知道,需要找出基线条件 和 递归条件,符合递归条件的时候调用自己,基线条件则函数不再调用自己。 而算法图解则是用插图的方式详细讲解了 递归每一步计算机都发生了什么,并且解释了编程的一个重要概念--调用栈(call stack)
很多问题解决都使用了递归概念,图解举了一个小例子: 阶乘.
阶乘公式是f(n) = n*(n-1)*...*1 (x>=1); f(0)=1
阶乘的实现很简单:

function factorial(n) {
    if(n < 0) {
        return -1;
    }
    if (n === 0) {
        return 1;
    }
    return n * factorial(n -1);
}

忍不住想要分享下阶乘的调用栈示意图:
image.png

快速排序

快速排序是一个很经典的问题了。快排的思想是分而治之。

  1. 选择一个基准值。
  2. 将数组分成两个子数组,分大于和小于基准值
  3. 对这2个子数组进行快速排序。

基于这个思想,额外申请空间,实现一个特别容易理解的快排。

function quickSort1(arr) {
    if ( arr.length <= 1) {
        return arr;
    }
    const low = 0; 
    const high = arr.length - 1;
    const base = parseInt(low + (high - low)/2);
    const baseValue = arr[base];
    const left = [];
    const right = [];
    const mid = [];
    arr.forEach((item, index) => {
        if (item < baseValue) {
            left.push(item);
        } else if (item > baseValue) {
            right.push(item);
        } else if (item === baseValue) {
            mid.push(item);
        }
    });
    return quickSort1(left).concat(mid).concat(quickSort1(right));
}

这个实现缺点是需要额外申请空间。我们知道快排关键是分区: 也就是将数组移动成 基准值左边均小于基准值,基准值右边均大于基准值。所以分区的关键是 找到基准值在数组中的位置。然后对两边的数组再进行快排。
分区过程是利用左边和右边两个指针。

  1. 左边元素小于 基准值 则左边指针向右移动。右边元素如果大于基准值,则右边元素向左移动。
  2. 当两个指针都停止移动时,交互两个元素的值。
  3. 当左右指针相同时,退出循环,此时的位置就是基准值应该在数组中的位置。
  4. 对位置左右两侧的数组重复上述过程排序

不是太好理解,在b站找视频看吧,或者看看算法4.
实现:

function quickSort3(arr, low, high) {
    if (low < 0 || high < 0 || low >= high || !arr.length) {
        return arr;
    }
    let pivot = arr[low];
    let i = low, j = high;
    while(i < j) {
        while (j > i && arr[j] > pivot) {
            j--;
        }
        while( i < j && arr[i] < pivot) {
            i ++;
        }
        if (i < j && arr[i] === arr[j]) {
            i ++;
        } else if ( i < j) {
            const temp = arr[j];
            arr[j] = arr[i];
            arr[i] = temp;
        }
    }
    quickSort3(arr, low,  i - 1);
    quickSort3(arr, j + 1, high);
    return arr;
}

广度优先搜索

广度优先是由于图的查找,可以解决2类问题:

  1. 是否有路径
  2. 最短路径

是否有路径

一个现实生活中的demo。
你需要找芒果销售商,将芒果卖给他。你需要在朋友中查找芒果销售商,你可以这么做:

  1. 创建一个朋友名单
  2. 依次检查每个人,看他是否为芒果销售商
  3. 如果你的朋友没有,那就在朋友中的朋友找
  4. 直到找到销售商或者把朋友找完

解决这个问题,首先需要创建一个图,表明你朋友和朋友的朋友之间的关系。
具体实现如下

function is_seller(name) {
    return /m$/.test(name);
}
function createGraph() {
    return  {
        you: ['alice', 'bob', 'claire'],
        bob: ['anuj', 'peggy'],
        alice: ['peggy'],
        claire: ['thom', 'jonny'],
        anuj: [],
        peggy: [],
        thom: [],
        jonny: []
    };
}
export default function breadth_first_search(name) {
    const graph = createGraph();
    // one queue
    const searchQueue = [];
    const searched = [];
    searchQueue.push(...graph[name]);
    let person;

    while(searchQueue.length) {
        person = searchQueue.shift();
        // not search this person
        if (!searched.includes(person)) {
            searched.push(person);
            if (is_seller(person)) {
                console.log('the person is', person);
                return true;
            } else {
                searchQueue.push(...graph[person]);
            }
        }
    }
    return false;
};

迪克斯特拉算法

迪克斯特拉算法用于解决加权图中前往x的最短路径。
迪克斯特拉算法步骤是

  1. 找出 ’最便宜‘的节点
  2. 更新该节点邻居的开销
  3. 重复这个过程,直到对每个节点都这样做了。
  4. 计算最终路径

以下图为例,利用一个迪克斯特拉算法查找从 起点到终点的最短路径:
image.png

要编写解决这个问题的代码,需要三个散列表

  1. 构建各个节点的图
  2. 表明每个节点代价的object
  3. 表明每个节点parent的object

参考实现

function find_lowest_cost_node(costs, processed) {
    let lowest_cost = Infinity;
    let lowest_node = '';
    for (let node in costs) {
        if ( lowest_cost >= costs[node] && !processed.includes(node)) {
            lowest_cost = costs[node];
            lowest_node = node;
        }
    }
    return lowest_node;
}
function dikstra(graph, costs, parent) {
    const processed = [];
    let node = find_lowest_cost_node(costs, processed);
    while(node && !processed.includes(node)) {
        Object.keys(graph[node]).forEach(item => {
            // if item cost less than costs ,than update the costs and parent
            if (costs[node] + graph[node][item] < costs[item]) {
                costs[item] = costs[node] + graph[node][item];
                parent[item] = node;
            }
        });
        processed.push(node);
        // update node
        node = find_lowest_cost_node(costs, processed);
    }
    // find the best path through parent
    const paths = ['fin'];
    node = 'fin';
    while(parent[node]) {
        paths.unshift(parent[node]);
        node = parent[node];
    };
    // console.log(paths);
    console.log('the start to fin path is:', paths.join('-'), 'the costs is: ', costs['fin']);
};

let graph = {
    start: {
        a: 5,
        b: 2
    },
    a: {
        c: 4,
        d: 2
    },
    b: {
        a: 8,
        d: 7
    },
    c: {
        fin: 3,
        d: 6
    },
    d: {
        fin: 1
    },
    fin: {

    }
};
let costs = {
    a: 5,
    b: 2,
    c: Infinity,
    d: Infinity,
    fin: Infinity
};
let parent = {
    a: 'start',
    b: 'start',
    c: '',
    d: '',
    fin: ''
};

dikstra(graph, costs, parent);

动态规划

第9章讲了动态规划,其实我现在也没有完全搞懂动态规划。书中对于动态规划的讲解主要是 图解每一步,对于 状态转移方程 如何推导,确实没有怎么讲。所以对于这一节内容,我也只是停留在实现,更深入的还需要看其他资料。
写一个 最长公共子序列作为参考吧

function getTemp(arr, i, j) {
    if (i < 0 || j < 0) {
        return 0;
    } 
    return arr[i][j];
    
}
const longestCommonSubsequence = function(text1, text2) {
    if (!text1.length || !text2.length) {
        return 0;
    }
    const len1 = text1.length;
    const len2 = text2.length;
    let res = [];
    for (let i = 0; i < len1; i ++) {
        res[i] = [];
        for(let j = 0; j < len2; j ++) {
            if (text2[j] === text1[i]) {
                res[i][j] = getTemp(res, i - 1, j - 1) + 1;
            } else {
                res[i][j] = Math.max(getTemp(res, i - 1, j), getTemp(res, i, j - 1));
            }
        }
    }
    return res[len1 -1][len2-1];
};
console.log(longestCommonSubsequence('abcde', 'ace'));

侯贝贝
329 声望27 粉丝