一、前言
算法&数据结构,一听这两个词就有点望而生畏,一是觉得工作上接触少之又少,二是觉得学习成本较高,所以平时用不到,又难啃的知识必然提不起学习的兴趣。所以首先要了解它们存在的意义,以及在工作中实际的运用场景,换句话说,勾起我们的兴趣,觉得这玩意真的有用。开始之前,先定个小目标,把它的基础概念搞清晰!
二、为什么学
2.1底层的黑魔法不再陌生
现在前端开发各种框架、库、工具层出不穷,很多年轻前端,利用这些工具每天兢兢业业的进行着上层建筑,你知道用connect可以绑定组件和store的联系,知道setState可以改变组件状态,知道promise、setTimeOut可以处理异步操作,但如果有一天,你突然开始好奇这里面的原理了,你不想再对底层只有一个黑魔法的印象。就像是现在的自己开始看算法数据结构了。 算法和数据结构是一种解决问题的思想,我们利用计算机解决实际的问题,指令是我们操作计算机的媒介,开发工程师的工作是把实际的问题转化成计算机指令,如何转化?
设计出数据结构,再施加以算法就行了。(引言《数据结构》)
2.2成长的必经之路
看看当今各大公司面试对待算法和数据结构的态度就知道了。 Google、Microsoft面试时,算法和数据结构起决定性作用 国内某些大厂面试时,算法和数据结构作为重要参考因素 中小型企业面试时,算法和数据结构作为加分项 外包不做要求
所以说如果你想成长(升职加薪)那就避不开这个的。
三、基础概念
开始之前,先夯实基础知识,时间复杂度和空间复杂度共同决定着一段代码质量的好坏
3.1时间复杂度
算法中基本操作重复执行的次数(频率)称为算法的时间复杂度,一个算法的时间复杂度影响着程序开始到结束的运行时间。 没有循环语句,记作o(1)。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n)
常见的时间复杂度
O(1): Constant Complexity: Constant 常数复杂度
O(log n): Logarithmic Complexity: 对数复杂度
O(n): Linear Complexity: 线性时间复杂度
O(n^2): N square Complexity 平方
O(n^3): N square Complexity ⽴⽅
O(2^n): Exponential Growth 指数
O(n!): Factorial 阶乘
3.2空间复杂度
空间复杂度指运行完一个程序所需内存的大小,利用其原理可以预先评估程序运行所需内存。
四、数据结构-基础知识
数据结构到底是什么?
数据结构即数据元素相互之间存在的一种和多种特定的关系集合。 一般分两个维度去了解它,逻辑结构和存储结构
4.1逻辑结构
逻辑结构就是数据之间的关系,大概分为两种:线性结构、非线性结构。
线性结构:有序数据元素的集合,元素之间一对一的关系,即除了第一个和最后一个,其余都是首尾相接。 常见的线性结构:栈、队列、链表、线性表
非线性结构:数据元素可能和0个或多个元素发生联系 常见:二维数组、树
4.2存储结构
逻辑结构指的是数据间的关系,存储结构是在计算机中存储的实现。常见的存储结构有顺序存储(数组)、链式存储(链表)、索引存储、散列存储(哈希表)。
4.3数据结构-二叉树
树最简单、应用最广泛的种类-二叉树
二叉树每个节点至多有两个子节点,左子树、右子树
4.3.1二叉树遍历
一棵二叉树由根结点、左子树和右子树三部分组成
有三种代表性的遍历方式
前序遍历:根左右 (前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。)
中序遍历:左根右
后序遍历:左右根
前中后实际上是指访问根节点的次序
递归实现前序遍历
var preorderTraversal = function (root, array = []) {
if (root) {
array.push(root.val);
preorderTraversal(root.left, array);
preorderTraversal(root.right, array);
}
return array;
};
非递归实现前序遍历
var preorderTraversal = function (root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length > 0) {
while (current) {
result.push(current.val);
stack.push(current);
current = current.left;
}
current = stack.pop();
current = current.right;
}
return result;
};
扩展:根据前序中序的特点反推二叉树 先序:ABCDEFG 中序:CBEDFAG
思路
- 前序遍历找到根结点root
- 找到root在中序遍历的位置 -> 左子树的长度和右子树的长度
- 截取左子树的中序遍历、右子树的中序遍历
- 截取左子树的前序遍历、右子树的前序遍历
- 然后左右子树也分别为二叉树,由此可以递归来解决问题
另外还有很多如求二叉树深度、搜索二叉树等经典算法,就不一一列举了,这里只做一个入门级讲解
4.4数据结构-链表
链表是一种物理存储单元上非连续、非顺序的存储结构(区别与数组),数据元素的逻辑顺序是通过链表中的指针链接次序实现的。所以链表的一个对象存储着本身的值和下一个元素的地址。
特点:
需要遍历才能查询到元素,查询慢。(不像数组直接arr[2],指定下标定位) 增删元素效率高,只需断开连接重新赋值。(不像数组插入个元素,其后的物理位置都被打乱了)
React16的Fiber Node连接起来形成的Fiber Tree,就是通过链表结构实现的
React16提出了Fiber结构,其能够将任务分片,划分优先级,同时能够实现类似于操作系统中对线程的抢占式调度,非常强大。
4.4.1基本应用
从尾到头打印链表 要遍历链表就是不断找到当前节点的next节点,当next节点是null时,说明是最后一个节点,停止遍历。 因为是从尾到头,所以用unshift从头部插入
function printListFromTailToHead(head)
{
const array = [];
while(head){
array.unshift(head.val);
head = head.next;
}
return array;
}
删除链表中的节点
条件:给定单链表的头指针和要删除的指针节点 分成三种情况 删除的是尾节点,且没有上一个节点 删除的是尾节点,且有上一个节点 删除的不是尾节点,将next节点覆盖当前节点
var deleteNode = function (head, node) {
if (node.next) {
node.val = node.next.val;
node.next = node.next.next;
} else if (node === head) {
node = null;
head = null;
} else {
node = head;
while (node.next.next) {
node = node.next;
}
node.next = null;
node = null;
}
return node;
};
五、算法
五大常见算法 分治、贪婪、动态规划、回溯、穷举
5.1排序
5.1.1快速排序
引言:选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排 `
function quickSort (arr) {
if (array.length < 2) {
return array;
}
var target = arr[0]
var left = []
var right = []
for (let i = 0; i < arr.length; i ++) {
if (arr[i] < target) {
left.push(arr[I])
} else {
right.push(arr[I])
}
}
return quickSort(left).concat([target], quickSort(right))
}
`
5.1.2冒泡排序
引言:循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,交换位置。依次对每一对相邻元素进行上面的操作,不循环已经排序好的数。 `
function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len - 1; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) { // 相邻元素两两对比
var temp = arr[j+1]; // 元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
`
5.2贪心算法
思想:在对问题求解时,总是做出在当前看来是最好的选择。
适用场景:问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。子问题最优解称为最优子结构。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。贪心算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,省去了为找最优解要穷尽所有可能而必须耗费的大量时间。贪心算法采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择,就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解。虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪心算法不要回溯
5.2.1零钱问题
假设有25分、10分、5分、1分的硬币, 先要发给客户41分的零钱,如何办到硬币个数最少? 首先选择一张面额最大的人民币,即25分,然后在剩下的16分中选择金额最大的10分
5.2.2背包问题
7个物品重量分别wi=[35,30,60,50,40,10,25],价值分别是 pi=[10,40,30,50,35,40,30] 一个包承载重量150,怎么放,价值最高
策略1:价值主导选择,每次都选价值最高的物品放进背包;
策略2:重量主导选择,每次都选择重量最轻的物品放进背包;
策略3:价值密度主导选择,每次选择都选价值/重量最高的物品放进背包。
不同策略得出的都是当前的最优解,却不一定是整体最优的解。
优点:简单,高效,省去了为了找最优解可能需要穷举操作,通常作为其它算法的辅助算法来使用;
贪心算法也存在如下问题:
1、不能保证解是最佳的。因为贪心算法总是从局部出发,并没从整体考虑 ;
2、贪心算法一般用来解决求最大或最小解 ;
3、贪心算法只能确定某些问题的可行性范围
5.3穷举
穷举法(枚举法)的基本思想是:列举出所有可能的情况,逐个判断有哪些是符合问题所要求的条件,从而得到问题的全部解答。 它利用计算机运算速度快、精确度高的特点,对要解决问题的所有可能情况,一个不漏地进行检查,从中找出符合要求的答案。 实现上基本上是for循环嵌套
5.4分治
字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。 这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序) 任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。 问题的规模越小,越容易直接求解,解题所需的计算时间也越少。
如果分治法分解出的子问题的解,不能合并为该问题的解,建议用用贪心法或动态规划法
5.5动态规划算法
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。核心就是记住已经解决过的子问题的解。
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
求解步骤 (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状况
5.6回溯算法
回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
用回溯算法解决问题的一般步骤:
1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
回溯算法说白了就是穷举法。不过回溯算法使用剪枝函数,剪去一些不可能到达 最终状态(即答案状态)的节点,从而减少状态空间树节点的生成。 而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
5.7贪心算法、动态规划、回溯的区别
贪心算法与动态规划的不同在于它对每个子问题的解决方案都作出选择,不能回退,动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能,而回溯算法就是大量的重复计算来获得最优解。
六、小结
到此基本上实现了把算法和数据结构的基础概念搞清晰的小目标,至此打开了算法和数据结构的大门,可以通过剑指offer、leetcode继续深入学习。