Javascript简易算法笔记: 递归、简单的数据结构

本文是关于JavaScript算法基础的学习笔记,重点介绍了递归的基本概念和应用,如求和、求第N项的值。接着,文章探讨了二叉树的先序遍历实现,并引入了图的概念,包括邻接链表存储和深度优先、广度优先遍历。这些基础知识对于理解和运用递归及数据结构至关重要。

前言

注意: 本文不是新系列,只是学习算法时偶尔用到的笔记(刷题用的),不定期更新

这是一篇关于算法基础的文章,不涉及任何数学分析,只有最最基础的逻辑思维过程。大部分内容是个人理解,每个人想法自然迥异,欢迎讨论。

Q: Javascript里面有算法吗?
A: 有。
Q: 在哪?
A: 任何框架的源码里。

好了,言归正传吧。

开始:理解递归

关于递归,每本书的概念可能各不相同,有的细致、有的粗略,但表示的含义都是一致的,详细概念这里就略过,我在这里只做总结:

  1. 递归的关键点在于简化、归纳问题,而不是分解问题。
  2. 递归的本质是自己依赖自己,换言之,要能够分析自身过程的依赖规律

下面给定两个例子,方便理解递归过程是怎样工作的。
注意:
下面的示例为实验参考,请不要在生产代码使用。

求和

给定一个数列:

1 2 3 4 5 6 7 … N

N项的和Sn
假设n项的数列和为S(n), 那么:

n=1  S(1) = 1                // 极限情形
n=2  S(2) = 2 + S(1)     // S(2) 依赖于S(1)。
n=3  S(3) = 3 + S(2)
.... 

所以N项时,其规律为:

S(n) = n + S(n-1)

写成Javascript为:

    // show 是一个工具函数
    function show(...args){
        let str = args.join(' ');
        document.write("<p>"+str+"</p>")
    } 

    // 正文
    function sum(n){
        
        if(n==1) return 1
        return n + sum(n-1)
    }
    show(sum( 3 ))  // 6
    show(sum( 4 ))  // 10
    show(sum( 100 )) // 5050

求第N项的值

给定一个数列:

1 2 3 6 12 24 48 96 192 384 …

求第N项的
假设第N项的值为F(n);那么:

n=1  F(1)=1     // 极限情形1
n=2  F(2)=2     // 极限情形2
n=3  F(3)= F(1)+F(2) = 3             // 多次依赖
n=4  F(4)= F(1)+F(2)+F(3) = 6   
n=5  F(5)= F(1)+F(2)+F(3)+F(4)=12
... ... 

伪代码:

f(n){
    if n=1; return 1;
    if n=2; return 2;
    
    n2= n, sum=0
    while(n2) sum+=f(--n2)    // 多次依赖的时候,可以用循环
    return sum 
}

写成Javascript代码为:

    function f(n){
        if(n==1) return 1
        if(n==2) return 2 
        
        let n2 = n, s =0
        while(n2) 
           s+=f(--n2)
        return s;
    }
    show(f( 3 ))  // 3
    show(f( 4 ))  // 6
    show(f( 10 )) // 384

okay, 基本上用数列入门递归是最简单的。记住这种感觉即可。


为简单起见,这里使用最简单的一种——二叉树

描述:

二叉树中,,每个节点只能有一个父节点指向自己(根节点除外),每个节点至多只有两个子树(或树分支节点)。

假设创建节点两侧分支的过程为createTree (T,h), 其中,T是一个根节点h深度(或高度)计数h=0时停止创建。 下面的抽象中,根节点是最深的。

h=0时,
在这里插入图片描述

createTree (T,0)
    return ;         

附注:

  • 相当于什么也不做,立即退出。

h=1时,
在这里插入图片描述

createTree (T,1)
    创建T的两侧分支B1 B2   
    

附注:

  • h=1时, 相当于为根节点建立左右两侧的分支B1B2

h=2时,
在这里插入图片描述

附注:工作流程如下:

在这里插入图片描述附注:

  • h=0时, createTree()会结束。
  • h=1时, createTree(T)会只会创建T的两侧分支。

整理上面的过程,即:

createTree(A, 2)
    创建A的两侧分支 B1 B2;
    createTree(B1,2-1)        // 创建B1的两侧分支, B1与A的深度不同,    // 过程依赖1
    // 当上个函数执行完毕后, B1的两侧分支完成。
    createTree(B2, 2-1)       // 创建B2的两侧分支                                         // 过程依赖2

h=3时,树结构如下:
在这里插入图片描述工作流程图:
在这里插入图片描述
综上所述,如果h=N时,那么:

createTree (T,N)
    创建T的两侧节点leftNode、rightNode 
    createTree (T.leftNode, N-1)			// 过程依赖
    createTree (T.rightNode, N-1)          // 过程依赖

因此,写成Javascript代码后就是:

   // 工具函数
   function show(...args){
       let str = args.join(' ');
       document.write("<p>"+str+"</p>")
   } 
   function initData(){
       return parseInt(Math.random()*1000)%1000
   }
   var index= 0;
   var visit = show; 
   
   // 正文
   function TreeNode(){
       this.id = index++
       this.data = initData()
       this.leftChild = null 
       this.rightChild = null 
   }
   var root = new  TreeNode() // 根节点
   // T:树节点
   // h:深度
   function createTree(T, h){
       if(h==0) return ;
       T.leftChild = new TreeNode()
       T.rightChild = new TreeNode() 
       createTree(T.leftChild , h-1)
       createTree(T.rightChild , h-1)
   }

仍然是简单起见, 这里只做一个先序遍历
代码如下:

   function preOrderTravel(root){
       if(!root) return ;
       visit(root.id, ' : ', root.data)   
       preOrderTravel(root.leftChild)
       preOrderTravel(root.rightChild)
   }
   var root = new  TreeNode() // 根节点
   createTree(root,3)
   preOrderTravel(root)

输出如下:

// 随机数!
0 : 438

1 : 646

3 : 906

5 : 523

6 : 540

4 : 604

7 : 505

8 : 990

2 : 250

9 : 591

11 : 427

12 : 657

10 : 118

13 : 492

14 : 146

小总结:

  • 递归相当于完成当前过程中,需要依赖于之前的自身过程,找到依赖前后的状态非常重要。
  • 这里的数据结构最简单的,而且也不怎么规范,仅用于理解

图也是最常见的一种数据结构,还是老规矩,省略概念,我这里只指出关键点,例如:
在这里插入图片描述上面是一个简单的有向图。重点如下:

  1. 如果v1能够通过一条路径访问到顶点v2,那么就说v1v2连通,反之则是不连通的。
  2. 如果v1v2连通的,那么之间的路径(它可以有方向)称作

图有很多种类,例如无向图多重图平面图等等。为便于理解,这里只使用最简单有向图

图有很多存储方式,例如邻接链表邻接矩阵邻接集合等等。在Javascript中,可以使用数组来模拟邻接链表存储方式。

邻接链表存储方式:

  1. 主链表是用于存放图中所有的顶点信息。
  2. 子链表是用于存放顶点周边的所有边的信息

上面的有向图,邻接链表存储后就是:
在这里插入图片描述


一个顶点Vertex构造信息如下:

function Vertex(){
    this.id = index++          // 顶点ID 
    this.data = initData()     // 顶点存放的数据
    this.arcNodes = []         // 边信息
    // 注意:
    //  arcNodes 实际上存放的是 Edge 数据结构,
    //  但此处省略了大量细节(例如边的权重等)
    //  所以arcNodes中实际上是周边连通顶点,特此说明。
}

然后再设置一个工具函数,用于连通两个顶点
注意,连接两个顶点是单向连通的。

// p -> q
function connect(graph, p , q){
    graph[p].arcNodes.push(graph[q])    // 
}

最终,创建图的方法如下:

// graph: 图链表
// maps: 需要连通的顶点映射
// max:  图中的顶点最大数量
function createGraph(graph, maps, max){
    for(let i = 0; i < max; i++) 
        graph[i] = new Vertex() 
    
    for(let i = 0; i < maps.length; i++)
        connect(graph, maps[i].source, maps[i].target)
}

假定要创建下面的图:
在这里插入图片描述
映射关联如下:

source   target
   0        5
   0        4
   5        6
   5        7 
   5        1 
   1        8
   4        8 
   4        2 
   2        3 
   3        0 

即:

     
   var maps= [   {source:  0,  target:  5 },
                 {source:  0,  target:  4 },
                 {source:  5,  target:  6 },
                 {source:  5,  target:  7 }, 
                 {source:  5,  target:  1 }, 
                 {source:  1,  target:  8 },
                 {source:  4,  target:  8 }, 
                 {source:  4,  target:  2 }, 
                 {source:  2,  target:  3 }, 
                 {source:  3,  target:  0 } ]
   
   var graph = []    // 图
   createGraph(graph, maps, 10)

图搞好了之后, 真正开始正文:

深度优先遍历

描述:

深度优先遍历的思想是从当前顶点层层深入,相对于当前顶点的顶点遍历,更优先于对当前顶点的周边连通顶点的顶点遍历。

就以这张图来说,深度优先遍历的策略就是:

v0->v5->v6 < ; 
v0->v5->v7 <;
v0->v5->v1->v8 <<<
v0->v4->v8 <
v0->v4->v2->v3->v0 

现在设深度优先遍历过程为DFS(g, v) ; 伪代码如下:

   DFS(g , v) 
      0 访问当前顶点v,并将当前节点标记。
      1 如果当前顶点v存在连通顶点(设为vN),那么:
         1-0 如果 vN未被访问, 那么DFS(g, vN)
         1-1 回到此层时,连通顶点vN的周边所有连通顶点必须遍历完成
         1-2 回到`1`。

写成Javascript为:

 function travel(g, v_id){     // g是图, v是当前顶点的id
     let marks = []  // 如果顶点已经被访问,那么marks[v_id]设为true。否则为undefined。
     function depthFirstSearch(g, v_id) {
         visit(v_id, ' : ', g[v_id].data)
         marks[v_id] = true 
         for(let vNode of g[v_id].arcNodes){
             if(!marks[vNode.id])
                 depthFirstSearch(g, vNode.id)
         }
     }
     depthFirstSearch(g, v_id)
 }
 travel(graph, 0)
 show('------')
 travel(graph, 1)
广度优先遍历

描述:

相较于深度优先遍历,广度优先遍历更优先于自身顶点的周边连通顶点的遍历。

如:

v0-> v5
v0->v4;
v5->v6;
v5->v7;
v5->v1;
v4->v8;
v4->v2;
v2->v3;
v3->v0;
 function travel_bfs(g, v_id){
     let marks = []
     function breadthFirstSearch(g, v_id){
         let queue = [ g[v_id] ]      
           // 将queue的顶点的周边连通顶点会被遍历。
         visit(v_id, ' : ', g[v_id].data)
         marks[v_id] = true 
         while(queue.length){
             let [v] = queue.splice(0, 1) 
             for(let vNode of v.arcNodes)
                 if(!marks[vNode.id]) {
                     visit(vNode.id, ' : ', vNode.data)
                     marks[vNode.id] = true 
                     queue.splice(queue.length, 0, vNode)
                 }
                     
         }
     }
     breadthFirstSearch(g, v_id)
 }
 show('------')
 travel_bfs(graph,0)

最后

请一定要注意,这是关于算法基础的文章,不是深入,基本上算法的深度取决于逻辑思维,算法的掌控程度取决于数学思维(个人见解), 因此如果希望深入算法学习的,一定会接触到高等数学, 逻辑和数学缺一不可

Q: 为什么要用Javascript?
A: 不想用C++了……(懒癌

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值