算法--线性结构--链表

递归(recursion)是函数式编程思想的产物,它使用数学函数的思想进行运算,只要在数学逻辑上是合理的,即代码中的函数一定合理。使用递归时,无须深究其运行过程!
栈的本质是递归。所以可以借助栈理解递归;先进后出,栈顶是递归终止条件,栈的每一层是递归的方法体。
斐波拉契数列的特点是:第1位和第2位固定为1,后面的位,其数字等于前两位之和,比如: [1, 1, 2, 3, 5, 8, 13, 21, …]
求斐波拉契数列第n位的值,n>0

如果使用函数f(n)来表示斐波拉契数列第n位的值,通过数学分析,可以轻松得到:

f(1) = 1
f(2) = 1
f(n) = f(n-1) + f(n-2)

以上等式考虑到了所有情况,并且在数学逻辑上是合理的,因此,可以轻松书写到代码中:

// 求斐波拉契数列第n位的值
function f(n){
 if(n === 1 || n === 2){
   	return 1;
 }
 return f(n-1) + f(n-2);
}

线性结构

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

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

数组

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

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

  1. 整个数组占用的内存空间是连续的

  2. 数组中元素的数量是固定的(不可增加也不可减少),创建数组时就必须指定其长度

  3. 每个元素占用的内存大小是完全一样的。

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

  1. 通过下标寻找对应的元素效率极高,因此遍历速度快(如果起始内存块地址为1000,每块内存的大小为20,那么下标为n的内存块的内存地址为1000+20*n),这样的话很方便根据下标进行查询。
  2. 无法添加和删除数据,虽然可以通过某种算法完成类似操作,但会增加额外的内存开销或时间开销
  3. 如果数组需要的空间很大,可能一时无法找到足够大的连续内存

JS中的数组

在ES6之前,JS没有真正意义的数组,所谓的Array,实际上是一个对象。可以进行增删改查操作

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

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

链表

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

  1. 每个元素除了存储数据,需要有额外的内存存储一个引用(地址),来指向下一个元素

  2. 每个元素占用的内存空间并不要求是连续的

  3. 往往使用链表的第一个节点(根节点)来代表整个链表
    在这里插入图片描述
    根据链表的基本特征,我们可以推导出它具有以下特点

  4. 长度是可变的,随时可以增加和删除元素

  5. 插入和删除元素的效率极高

  6. 由于要存储下一个元素的地址,会增加额外的内存开销

  7. 通过下标查询链表中的某个节点,效率很低,因此链表的下标遍历效率低

练习1: 打印整个链表的数据

     /**
 * 打印整个链表的数据
 * print(node) = 如果自己没有值,则不打印
 * print(node) = 打印自己 + 打印下一个
 * @param {*} node
 */
        function print(node){
              if(!node){
               return;
         }
        console.log(node.value);
        print(node.next)
       }  
       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     a.next=b;
     b.next=c;
     print(a);//aaa bbb ccc

二: 获取链表的长度

      /**
        * 获取链表的长度
 * @param {*} node
 */
        function len(node){
              if(!node){
               return 0;//如果没有节点,长度为0.
               //这里不能是return; 递归回去之后和前面的1相加有问题
         }
           return 1+len(node.next);
       }  
       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     a.next=b;
     b.next=c;
     console.log(len(a));

三:根据下标,获取链表中某个位置的数据

 /**
* 根据链表、寻找的下标、当前的下标,找到对应的数据
   * @param {*} node 根据链表
   * @param {*} index 寻找的下标
   * @param {*} curIndex 当前的下标
 * @param {*} node
 */
        function find(node,index){
            var currentIndex=0;
            //因为缺少一个参数,表示当前的下标,所以在里面重建一个函数。
             function findinner(node,index,currentIndex)
             {   
                if (!node) {
                 // 超出了链表的范围
                  throw new Error("超出了下标范围");
                 }
                 if(index===currentIndex){
                     return node.value;
                 }else{
                    currentIndex++;
                    return findinner(node.next,index,currentIndex);

                 }
             }
             return findinner(node,index,currentIndex)
       }  
       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     a.next=b;
     b.next=c;
     console.log(find(a,2));

四:根据链表、寻找的下标、当前的下标,设置对应的数据

 /**
        通过下标设置链表中的某个数据
 * @param {*} node
 */
        function set(node,index,value){
            var currentIndex=0;
             function setinner(node,index,currentIndex)
             {   
                if (!node) {
                 // 超出了链表的范围
                  throw new Error("超出了下标范围");
                 }
                 if(index===currentIndex){
                    node.value=value;
                    return ;
                 }else{
                    currentIndex++;
                    return setinner(node.next,index,currentIndex);

                 }
             }
             return setinner(node,index,currentIndex);
       }  
       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     a.next=b;
     b.next=c;
     console.log(set(a,2,'新的ccc'));

五:在链表node的末尾,加入一个新的节点,值为value

/**
 * 在链表node的末尾,加入一个新的节点,值为value
 * @param {*} node
 * @param {*} value 新节点的值
 */
function push(node, value) {
  if (node.next) {
    push(node.next, value); //继续往后看
  } else {
    // 到了链表的末尾
    var newNode = new Node(value);
    node.next = newNode;
  }
}
       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     a.next=b;
     b.next=c;
     console.log( push(a, 'ddd') );

六:在链表node中,寻找beforeNode,在其之后,加入一个新节点,新节点的值为value

/**
 * 在链表node中,寻找beforeNode,在其之后,加入一个新节点,新节点的值为value
 * @param {*} node
 * @param {*} beforeNode
 * @param {*} value
 */
function insert(node,beforeNode,value){
    if(node===beforeNode){
        var newnode=new Node(value);
        //想要形成a->b->g->c->d的链表,如果先让b.next指向新节点,那么就失去了对c节点的引用(没有节点指向它,c节点表示不出来了),后面新节点.next指向c也无法实现
       //所以要将新节点.next指向c节点,此时b节点.next也是指向c节点的,然后让b节点.next指向新节点。
        //newNode.next = node.next;
        //node.next = newNode;
        newnode.next=beforeNode.next;
        beforeNode.next=newnode;

    }
    else{
        insert(node.next,beforeNode,value)
    }
}

       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     var d=new Node('ddd');
     a.next=b;
     b.next=c;
     c.next=d;
     console.log(insert(a,b, 'ggg') );

七:在链表node中,寻找removeNode,并删除它

/**
 * 在链表node中,寻找removeNode,并删除它
 * @param {*} node
 * @param {*} removeNode
 */
function remove(node,removeNode){
    if(node.next===removeNode){
       //找删除的节点位置的前一个节点,并改变它的指向,指向删除节点的下一个节点。
        node.next=removeNode.next;

    }
    else{
        remove(node.next,removeNode)
    }
}

       /**
 * 链表节点
 * @param {*} value
 */
       function Node(value){
        this.value=value;
        this.next=null;
       }
     var a=new Node('aaa');
     var b=new Node('bbb');
     var c=new Node('ccc');
     var d=new Node('ddd');
     a.next=b;
     b.next=c;
     c.next=d;
     console.log( remove(a,c) );

八、反转链表
1.双指针迭代
我们可以申请两个指针,第一个指针叫 pre,最初是指向 null 的。
第二个指针 cur 指向 head,然后不断遍历 cur。把cur.next先保存起来,以防后面引用不到它。
每次迭代到 cur,都将 cur 的 next 指向 pre,然后 pre 和 cur 前进一位。
都迭代完了(cur 变成 null 了),pre 就是最后一个节点了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
进入下一次操作
在这里插入图片描述
图来源自leetcode大佬

var reverseList = function(head) {
var pre=null;
var cur=head;
while(cur!=null){
    var temp=cur.next;
    cur.next=pre;
    pre=cur;
    cur=temp;
}
return pre;
};

补充:
如果将pre设置为head,cur设置为head.next就会超时,这是为什么?
在这里插入图片描述
递归的思路:
在这里插入图片描述
在这里插入图片描述其他节点的操作同理,最终:
在这里插入图片描述

递归的两个条件:

终止条件是当前节点或者下一个节点==null
在函数内部,改变节点的指向,也就是 head 的下一个节点指向 head 递归函数那句head.next.next = head,很不好理解,其实就是 head 的下一个节点指向head,从而实现了反转。
递归函数中每次返回的 cur 其实只最后一个节点,在递归函数内部,改变的是当前节点的指向。(看完之后再理解)。

var reverseList = function(head) {
    if(head == null || head.next == null){
        return head
    }
    const cur= reverseList(head.next);
    //层层递归,返回最后一个节点

    //从节点4开始进行反转操作
    //1.实现head和head.next之间的反转
    head.next.next = head;
    //2.把head.next设置为null,切断head和head.next正方向的指针
    head.next = null
    //每层递归返回最后一个节点,因为每个递归相对于cur而言,没有操作cur,cur的值不变,保留其作为反转链表的开始节点。
    return cur;
};

两个tip帮助理解:

1.当指针指向节点5时,根据函数终止条件会直接返回
于是返回到上一层,这时候节点就指向了4。
cur指向的是返回后的节点所以就是5,head指向当前节点所以就是4。
2.
在这里插入图片描述
3.递归的思路:
在这里插入图片描述
定义了 reverseLis t函数的功能可以把⼀个单链表反转,在里面去递归调用reverseList(head.next),实现了对 2->3->4反转。
在这里插入图片描述
我们把 2->3->4 递归成 4->3->2。不过,1 这个节点我们并没有去碰它,所以 1 的 next 节点仍然是连接这 2。接下来就简单了,我们接下来只需要把节点 2 的 next 指向 1,然后把 1 的 next 指向 null,不就⾏了?,即通过改变 newList 链表之后的结果如下:
在这里插入图片描述
也就是说,reverseList(head) 等价于 ** reverseList(head.next)** + 改变⼀下1,2两个节点的指向。

var reverseList = function(head) {
   if((head===null)||head.next==null){
       return head;
   }
   var end=reverseList(head.next);
    通过 head.next获取节点2
   var node2=head.next;
   // 让 2 的 next 指向 1
   node2.next=head;
   // 1 的 next 指向 null.
   head.next=null;
    把调整之后的链表返回。
   return end;
};

九、合并两个有序链表:
套路:

1.哑节点

**创建 哑节点 作为 结果链表 的开头,刚开始指向的是一个空节点,返回结果是这个节点的下一个位置。**目的是:在未遍历之前,我们不知道构建的结果中,开头元素到底是 l1 还是 l2, 为了让代码整齐,创建哑节点。

2.使用 move 游标
哑节点标记了 结果链表 的开头,因此是不能移动的。为了把两个链表 merge 的结果放到结果链表的最后,就需要使用一个 move 游标指向 结果链表 的最后一个元素。初始时,move 指向 哑节点,之后随着结果链表的增加而不停地向后移动,始终保持其指向 结果链表 的最后一个元素。

while 遍历两个元素
涉及到两个元素的遍历题,使用 while l1 and/or l2 的方式。即两个元素都没有遍历完或者至少有一个没遍历完,具体使用 and 还是 or 要根据场景进行选择。
没用完的元素仍需拼接
当 while 循环结束之后,l1 和 l2 至少遍历完了一个,另一个链表可能没有用完,因此需要拼接到 结果链表 的结尾。

至于本题只需要判断两个链表头部元素的大小,把小的那个链表节点放到 结果链表 的结尾即可。

来自大佬的解释

var mergeTwoLists = function(l1, l2) {
    
//创建 哑节点 作为 结果链表 的开头,返回结果是这个节点的下一个位置。
//目的是:在未遍历之前,我们不知道构建的结果中,开头元素到底是 l1 还是 l2, 为了让代码整齐,创建哑节点。

var curr= new ListNode();
var pre=curr;//指向空节点的首结点
while(l1!=null&&l2!=null){
    if(l1.val<=l2.val)
    {    
        curr.next=l1;
        l1=l1.next;
       
        //相当于result.push(l2),l2++的操作
    }
    else
    {
        curr.next=l2;
        l2=l2.next;
    }
    //每次操作之后curr也往后移动
    curr=curr.next;
}
    if(l1===null){
        curr.next=l2;//将l2后面的链表关系保留放在curr.next的后面
    }
    if(l2===null){
        curr.next=l1;
    }
 return pre.next;
};

2.递归法:
在这里插入图片描述
在这里插入图片描述
注意l1和l2的指针式不断变化的,谁小谁就要往后移一位。以此类推
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
来自大佬的图

 function mergeTwoLists(l1,l2){
            if(l1===null){
                return l2;
            }
            else if(l2===null){
                return l1;
            }
            else if(l1.val<l2.val){
                l1.next=mergeTwoLists(l1.next,l2);
                return l1;
            }
            else{
                l2.next=mergeTwoLists(l1,l2.next);
                return l2;
            }
        }

两者代码一样,但是理解思路可以不一样,我也不明白为什么
总结:
1.写递归前,想好逻辑,看下哪个逻辑是重复使用的,这部分可以使用递归。
2.如果函数的参数不够用,需要再添加参数,可在函数里面再定义一个子函数。
3.写增加删除时要核实这个节点是否可以使用,被删除的节点(没有节点指向它)会被垃圾回收器自动回首。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值