1.0 什么是算法
1.《算法图解》这本书上对什么是算法做了一个简单的定义,算法是一组完成任务的指令,我的理解是任何可以用来完成计算任务或者提升计算效率的函数表达式或函数引用集合都可以称之为算法。算法就像一台加工机器,可以丢给他一种或若干中原料(参数)它可以帮我们生成出令我们满意的结果(返回值或者一种处理)。
<script>
function calc(a,b){
return a + b
}
console.log(calc(1,2)) // => 3
</script>
小结:上面的例子虽说简单但是却揭示的算法的一层含义,那就是解决计算问题。当我们给定参数 a = 1 , b = 2 时,calc1会帮我们计算出他们的和,因此可以帮我们解决计算问题, 当然我们还可以在里面加入更加复杂的逻辑判断,循环,递归等,帮助我们解决更加复杂的问题。
2.下面我们来看一看比较深入的例子
<script>
function calc(a,b){
let arr = [0,1,2,3,4]
let obj = {}
for(var i=0;i<arr.length;i++){
obj[i] = arr[i]
}
return obj[a] + obj[b]
}
console.log(calc(2,3)) // ==> 5
</script>
小结:上面的例子也很简单,也可以帮助实现一部分的计算任务,可是各位可能会发现一个问题,那就是每当我进行一次计算时,其中的arr 都会被遍历一次,都会被执行一次,而我们其实需要的就仅仅是哪个生成出来的obj 对象就好了,这样造成了许多不必要的遍历,因此在效率上是很不高的,我们需要改善一下。
`小结`:各位可能发现了,这个函数就比之前的函数效率要高很多,首先从一方面来看先将一部分的计算结果算出来缓存起来,然后再使用这个结果,不用再次重复生成使用结果的部分,这样一来,就避免了这种不必要的遍历和计算,节约开销,效率当然是提升了,因此这样能够提升计算效率的计算过程,我们也即可将其理解为算法具有的特点。
1.1 二分查找
<script>
function calc(){
let arr = [0,1,2,3,4]
let obj = {}
for(var i=0;i<arr.length;i++){
obj[i] = arr[i]
}
return function (a,b){
return obj[a] + obj[b]
}
}
let fun = calc()
console.log(fun(2,3)) // => 5
</script>
1.二分查找是一种提升查找效率的算法,查找的业务场景是很多的,比如你想打电话,你的手机上有一个通讯录,你需要通过查找来找到该联系人,此时你可以选择一个一个的从上往下找,直到找到这个联系人为止,你也可以选择通过首字母查找,很快便能够找到联系人,二分查找是基于排序数列的高效的查找方式,不过它必须有一个前提条件,那就是数列必须排序。
场景:有一个数组0-100,已经是排好序的,现在需要你找出数字为68的数字,要求效率越高越好?你会怎么做的,我们通常的做法可以是这样
`小结`:可以看到,如果在没有return 或者break阻断的情况下,要找到68这个数组,这个遍历查找的次数是100次,也就是说不管我们要查找的数字是多少,这个程序都会查找100次,如果数组比较庞大,是1000,10000次呢,那查找的次数就更多了,因此我们想了一下,其实我们可不可以这样,每次查找我们直接砍掉一半,比如第一次直接选最中间的那个数,判断一下小了还是大了,小了,说明目标数值在大的那一半,这样一下子就砍掉了一半,再继续找出剩下那一半,最中间的那个数值,继续判断,通过这样的方式,我们最多只要6次就可以找出任意一个数值,是不是效率就会有大大的提升呢?我们直接写一个函数试一下。
<script>
// 方法一:使用简单查找
function calc1(arr,target){
// 需求:一个函数,传递一个数组和一个目标值,以最快的速度找出这个目标值的位置 ,并打印计算次数
if(!Array.isArray(arr)){
return arr
}
let count = 0
let find
arr.forEach((item,index)=>{
count ++
if(item == target){
find = {item,index}
}
})
return {count,find}
}
let arr = []
for(var i=0;i<100;i++){
arr.push(i)
}
console.log(calc1(arr,68))
// count: 100
// find: {item: 68, index: 68}
</script>
<script>
// 方法二:二分查找
function calc2(arr,target){
// 需求:一个函数,传递一个数组和一个目标值,以最快的速度找出这个目标值的位置 ,并打印计算次数
if(!Array.isArray(arr)){
return arr
}
let low = 0;
let height = arr.length;
let find = {}
let count = 0
while(low < height){
let middle = parseInt((low + height)/2)
let guess = arr[middle] // 中间的那个值
count++
if(guess < target){ //猜小了
low = middle
} else if(guess > target){ //猜大了
height = middle
}else{
find = {item:guess,index:guess}
// return {count,find}
}
low ++
}
return {count,find}
}
let arr = []
for(var i=0;i<100;i++){
arr.push(i)
}
console.log(calc2(arr,68))
//count: 6
// find: {item: 68, index: 68}
</script>
小结:可以看到通过6次,我们就可以将目标值找出来了,因此,是不是比之前的100要少很多查找的开销了呢,因此同样是查找数值,这样的函数能够大大减小计算的开销,因此,好的算法真的是能够在性能以及效率上给与很大支持的。
总结:那到底怎么判断多少的次数呢,上面我们可以看到,我们每次都会砍掉一半,也就是说,每次都会淘汰一半,这是一个对数问题,比如我们要查找的数组长度是2, 1次就一定能查找出来,随便挑一个,看是不是,是就直接找出来了,不是的话,也找出来了,因为另一个就一定是,长度是4,就两次,是8 就3次,以此类推,似乎2的n次方便是目标数组的长度,所以我们逆运算一下,log以2为底,数组的长度的对数是多少便是查找次数。
1.2运行时间(大O表示法)
1.一般来说,考察一个算法到底好不好,运行时间是很重要的一个指标,同样的结果,一种算法比另外一种算法运行时间更快我们就可以认为他的效率会更高,那么在认识运行时间之前我们需要认识几个概念:
** 线性时间:如果计算的次数和列表的长度相同的话,我们就称这种计算的运行时间属于线性时间,就和上面的方法一一样。
** 对数时间:如果计算的次数和列表的长度是对数相关的话,我们就称这种计算的运行时间属于对数时间,就和上面的方法二一样。
2.认识大O表示法
大O表示法,表示的是一种算法在数量大越来越大的情况下,计算的增速的一种体现
**语法** :O(运行次数)
**example**
==>O(n) 线性时间
==>O(logn) 对数时间
==>O(n*logn)
==>O(n**2)
==>O(n!)
`提示`:一般情况下,越优质的算法往往在数量级越大的情况下,才会体现的更加明显,因为当数量级越大的情况下,优质算法的增速越快。
3.数组和链表,在javascript这门语言当中,我似乎还没有接触过链表的概念但是数组我们是很熟悉的,下面我们从计算机原理中认识一下数组和链表的区别:
计算器内存像极了我们去超市购物时一个又一个的存储柜,每一个存储柜都有一个地址,有的地址已经被占用了,有的没有等待被占用,如果我们使用数组的话,计算机就会给我们的数组分配多个柜子去存储数组中的每一个元素,因为数组是连续的,所以计算机需要找到连续有空的存储区域来进行分配,如果我们往这个数组中添加一个元素,而当前数组所在的区域刚好被占满的情况下,计算器需要重新分配一个空间来存储新的数组。所以数组的特点就是查询容易,添加难,而链表则不同,他属于跳跃式的,可以在任意若干个柜子进行存储,因为每一个柜子都存储这自己下游柜子的地址,根据第一个柜子就可以一直找到最后柜子所在的内存地址,因此链表是添加容易,读取难。因此从原理上来看,数组和链表各有优势。
因此我们可以得到下面的结论:
(1) 中间插入数值,链表更好 ,只需要修改上下游元素的指向就好了
(2) 跳跃的查找,数组更好
(3) 删除元素,链表更好
4.选择排序,通过使用选择排序我们可以创建一个排好序的数组。
<script>
function findSmall(arr){
// 要求:能找找出这个数组中最小的哪一个
if(!Array.isArray(arr)){
return arr
}
let smallest = arr[0]
let index = 0
arr.forEach((e,i)=>{
if(e<smallest){
smallest = e
index = i
}
})
return [smallest,index]
}
function sort(arr){
// 要求:能对这个数组进行排序
if(!Array.isArray(arr)){
return arr
}
let newArr = []
while(arr.length){
let res = findSmall(arr)
newArr.push(res[0])
arr.splice(res[1],1)
}
return newArr
}
console.log(sort([2,1,7,8,4,6,3,7,8]))
/*
0: 1
1: 2
2: 3
3: 4
4: 6
5: 7
6: 7
7: 8
8: 8
*/
</script>
`小结`:这是一种较为简单的排序方法,使用起来可以实现响应的目标但在效率上还是会有很大优化的空间,不过暂时我们可以去学习他的思路,后续我们对他进行升级。
1.3 栈 (调用栈)
1.概念:什么是栈,栈在计算机中是一种数据结构,我们可以将其理解为一个容器,这种容器有一个特点,就是不断的能放进去一个数据,也能不断的从中弹出一个数据,放进去一个数据,意味着内存空间要被占用,弹出一个数据意味着内存空间将会被释放。
我们可以简单的来看一个函数
<script>
function fun(){
console.log('fun函数执行')
child()
end()
}
function child(){
console.log('child执行')
}
function end(){
console.log('end执行')
}
fun()
// 这个例子非常简单,但却能说明调用栈的结构是什么样子的。
</script>
`小结`:在这个例子中,首先声明了3个函数,当调用fun()这个函数时,计算机为这个函数分配了一块内存空间,用于执行这个函数所需的变量的存储,输出'fun函数执行后',执行child(),此时由于fun函数并没有执行完,因此,计算机又为child分配一块内存,用于执行这个函数所需的变量的存储,而fun的内存空间,作用域等依然都是存在的,此时此时内存当中其实是有两个作用域,两块内存空间的,当child执行完成之后,所对应的内存空间会立马被销毁清除,释放内存,再继续执行end然后循环往复,最终end函数执行完成之后,fun函数也就算执行完成,整个调用栈将会被清空,程序执行完毕,这边这一个程序执行的过程和栈结构再其中起的作用,可以看到栈其实就是在其中不断的弹出和加入一个数据的过程,而这种过程我们可以将其叫做调用栈的执行过程。
1.4 快速排序
<script>
// 快速排序,使用分而治之的思想
// 对于元素个数是 0 1 2 的数组,我们可以认为是天然有序的,但在实际情况下我们要忽略为2的情况,因为为2的情况下顺序是不固定的
// 我们只需要研究长度大于等于2的数组的情况
function sort(arr){
if(arr.length==1 || !arr.length){
return arr
}else{
let middle = arr[0]
let less = arr.slice(1).filter(e=>{
return e<middle
})
let more = arr.slice(1).filter(e=>{
return e>middle
})
return [...sort(less),middle,...sort(more)]
}
}
console.log(sort([1,4,7,9,8,4,3,2,6])) //[1, 2, 3, 4, 6, 7, 8, 9]
</script>
1.5 广度优先搜索
1.概念:解决最短路径问题的算法被称为广度优先算法,是一种查找图的算法
我们先来了解一些基本的概念
图:什么是图,图是由节点和边组成的一种描述相邻节点之间关系的数据结构
队列:队列是一种进行数据的导入和取出的操作的数据结构
栈:栈是一种进行数据导入和导出的操作的数据结构
区别:那他们到底有什么区别呢?首先队列是遵循先进先出的顺序,像是一个隧道,先开进去车的(忽略超车的情况)一定是先出来的,而栈是先进后出的,像是一个木桶,放进去若干的东西,往往先放进去的却是最后拿出来的。这就是栈和队列的不同。
<script>
function findSeller(person){
if(person.job == 'seller'){
return true
}
return false
}
function find(person){
// 创建一个查找的队列
let findArr = []
findArr.push(person)
while(findArr.length){
let target = findArr.shift()
if(findSeller(target)){
return target
}else{
if(target.friend && Array.isArray(target.friend)){
target.friend.forEach(e=>{
findArr.push(e)
})
}
}
}
}
let person = {
job:'no',
friend:[
{
job:'waiter',
friend:[
{
job:'CTO',
friend:[
{
job:'doctor'
}
]
}
]
},
{
job:'CEO',
friend:[
{
job:'seller',
name:'小鹏'
}
]
}
]
}
console.log(find(person))
// find(person)
</script>
最后的话:写这篇博客是由于最近在学习算法相关的内容,也是正在看《图解算法》这本书,如果想要作为一名优秀的前端工程师,我认为我们还是需要了解算法以及想关的一些编程思想的,所以我将书中所有python代码全部用javascript还原了一遍,并做了一些拓展,成为一名优秀的前端工程师,一直在路上,也希望在我自己学习的过程中能够稍微有一点帮助对于正在看这篇文章的你,当然文章中还是有很多需要改进的地方,如果有异议的地方,还望能够在评论下方,留言,我一定会仔细阅读,并做相关的改正,最后谢谢正在看这篇文章的你,我们一起加油!