【从蛋壳到满天飞】JS 数据结构解析和算法实现-链表与递归

思惟导图

前言

【从蛋壳到满天飞】JS 数据结构解析和算法实现,所有文章大概的内容以下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)html

源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)node

所有源代码已上传 github,点击我吧,光看文章可以掌握两成,动手敲代码、动脑思考、画图才能够掌握八成。git

本文章适合 对数据结构想了解而且感兴趣的人群,文章风格一如既往如此,就以为手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,但愿对想学习数据结构的人或者正在学习数据结构的人群有帮助。github

链表与递归

  1. 已经从底层完整实现了一个单链表这样的数据结构,
    1. 而且也依托链表这样的数据结构实现了栈和队列,
    2. 在实现队列的时候对链表进行了一些改进。
  2. 递归不光用于树这样的结构中还能够用在链表这样的结构中
    1. 链表自己就自然的具备递归结构性质,
    2. 只不过链表太简单了,它是一个线性结构,
    3. 因此可使用非递归的方式,
    4. 如使用循环的方式就能够很是容易的解决链表的问题,
    5. 从链表开始就要打好递归的基础,
    6. 对深刻学习树结构包括更加深入的理解递归算法都是很是有好处的。
  3. 经过 leetcode 上与链表相关的问题来学习递归
    1. 在 leetcode 上提交链表相关的问题,
    2. 还有一些其它须要注意的地方,
    3. 与此同时在 leetcode 上解决与链表相关的问题,
    4. 思路在有一些地方和以前自自定义链表是不一样的,
    5. 这里面的关键不一样是在于有些状况下作这些程序
    6. 是以节点为中心的而不会包装一个总体的链表类。

leetcode 上与链表相关的问题

  1. 203 号问题:删除链表中的元素
    1. 先找到这个链表中这个节点以前的那个节点,
    2. 可是对于头节点来讲没有以前的那个节点,
    3. 因此就要特殊处理或者使用虚拟头节点来统一这个操做。

代码示例(class: Solution)

  1. Solution算法

    class Solution {
       removeElements(head, val) {
          /** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */
          /** * @param {ListNode} head * @param {number} val * @return {ListNode} */
          var removeElements = function(head, val) {
             // 对头步进行特殊处理
             while (head !== null && head.val === val) {
                head = head.next;
             }
    
             // 处理后的头部若是为null 那直接返回
             if (head === null) {
                return null;
             }
    
             // 由于头部已经作了特殊处理, head即不为null 而且 head.val不等于null
             // 那么能够直接从 head的下一个节点开始判断。
             let prev = head;
             while (prev.next !== null) {
                if (prev.next.val === val) {
                   let delNode = prev.next;
                   prev.next = delNode.next;
                   delNode = null;
                } else {
                   prev = prev.next;
                }
             }
          };
    
          return removeElements(head, val);
       }
    }
    复制代码

自定义 203 号问题测试用例

  1. 将数组转换为链表
    1. 链表的第一个节点就是你建立的这个节点,
    2. 这个节点的值也是数组的第一个值,
    3. 其它的节点经过第一个节点的 next 进行关联,
    4. 对应的值为数组中的每一个值。

代码示例

  1. (class: ListNode, class: Solution,数组

    1. class: Solution2, class: Main)
  2. ListNode数据结构

    class ListNode {
       constructor(val) {
          this.val = val;
          this.next = null;
       }
    
       // 将一个数组对象 转换为一个链表 而且追加到当前节点上
       appendToLinkedListNode(array) {
          let head = null;
          if (this.val === null) {
             // 头部添加
             head = this;
             head.val = array[0];
             head.next = null;
          } else {
             // 插入式
             head = new ListNode(array[0]);
             head.next = this.next;
             this.next = head;
          }
    
          // 添加节点的方式 头部添加、尾部添加、中间插入
    
          // 尾部添加节点的方式
          for (var i = 1; i < array.length; i++) {
             head.next = new ListNode(array[i]);
             head = head.next;
          }
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `ListNode: \n`;
          arrInfo += `data = front [`;
          let node = this;
          while (node.next !== null) {
             arrInfo += `${node.val}->`;
             node = node.next;
          }
          arrInfo += `${node.val}->`;
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  3. Solutionapp

    class Solution {
       // leetcode 203. 移除链表元素
       removeElements(head, val) {
          /** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */
          /** * @param {ListNode} head * @param {number} val * @return {ListNode} */
          var removeElements = function(head, val) {
             // 对头步进行特殊处理
             while (head !== null && head.val === val) {
                head = head.next;
             }
    
             // 处理后的头部若是为null 那直接返回
             if (head === null) {
                return null;
             }
    
             // 由于头部已经作了特殊处理, head即不为null 而且 head.val不等于null
             // 那么能够直接从 head的下一个节点开始判断。
             let prev = head;
             while (prev.next !== null) {
                if (prev.next.val === val) {
                   let delNode = prev.next;
                   prev.next = delNode.next;
                   delNode = null;
                } else {
                   prev = prev.next;
                }
             }
    
             return head;
          };
    }
    复制代码
  4. Solution2ide

    class Solution {
       // leetcode 203. 移除链表元素
       removeElements(head, val) {
          /** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */
          /** * @param {ListNode} head * @param {number} val * @return {ListNode} */
          var removeElements = function(head, val) {
             if (head === null) {
                return null;
             }
    
             let dummyHead = new ListNode(0);
             dummyHead.next = head;
             let cur = dummyHead;
             while (cur.next !== null) {
                if (cur.next.val === val) {
                   cur.next = cur.next.next;
                } else {
                   cur = cur.next;
                }
             }
             return dummyHead.next;
          };
    
          return removeElements(head, val);
       }
    }
    复制代码
  5. Main函数

    class Main {
       constructor() {
          this.alterLine('leetcode 203. 删除指定元素的全部节点');
          let s = new Solution();
    
          let arr = [1, 2, 3, 5, 1, 2, 1, 3, 5, 3, 5, 6, 3, 1, 5, 1, 3];
          let node = new ListNode(null);
          node.appendToLinkedListNode(arr);
    
          console.log(node.toString());
          let result = s.removeElements(node, 1);
          console.log(result.toString());
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

链表和递归

  1. 递归是极其重要的一种组建逻辑的机制
    1. 尤为是在计算机的世界中
    2. 对于高级的排序算法一般都须要使用递归,
    3. 对于计算机科学来讲熟练的掌握递归是极其重要的,
    4. 甚至能够说初级水平与高级水平之间差距的关键分水岭。
  2. 递归能够作
    1. 分形图形的绘制,
    2. 各类高级排序算法的可视化。

递归

  1. 本质上就是将原来的问题,转化为更小的一样的一个问题
    1. 也就是将问题规模逐渐缩小,小到必定程度,
    2. 一般在递归中都是小到不能再小的时候就能够很容易的解决问题,
    3. 这样一来整个问题就能够获得解决。

递归的使用例子

  1. 数组求和:求数组中 n 个元素的和

    1. Sum(arr[0...n-1]) = arr[0] + Sum(arr[1...n-1]) 第一次,
    2. Sum(arr[1...n-1]) = arr[1] + Sum(arr[2...n-1]) 第二次,
    3. ... 若干次
    4. Sum(arr[n-1...n-1]) = arr[n-1] + Sum(arr[]) 最后一次`
    5. 每一次都是将同一个问题慢慢更小化从而演化成最基本的问题,
    6. 最基本的问题解决了,而后根据以前的一些逻辑,从而解决原问题,
    7. 就像一个大问题,若是他能够分解成无数个性质相同的小问题,
    8. 而后对这些小问题以递归的形式进行处理,这样一来就容易多了。
    9. 代码中if (arr.length == cur) {return 0;}就是解决最基本的问题
    10. 代码中arr[cur] + sum(arr, cur+1);就是在构建原问题的答案
    11. 代码中sum(arr, cur+1);就是不断的将原问题转化为更小的问题,
    12. 不少个小问题的解加到一块儿,就构建出原问题的答案了。
    class Calc {
       constructor() {}
       // 递归求和
       sum(array, cur = 0) {
          // 解决最基本的问题
          if (cur === array.length) {
             return 0;
          }
          // 化归思想
          // 将原问题分解为性质相同的小问题
          // 将众多小问题的答案构建出原问题的答案
          return array[cur] + this.sum(array, cur + 1);
       }
       // 尾递归求和
       tailSum(array, cur = 0, result = 0) {
          // 解决最基本的问题
          if (cur === array.length) {
             return result; // 这里是上面的sum不同,这里直接返回最终计算结果
          }
          // 化归思想 : 将原问题分解为性质相同的小问题,使用小问题的解构建出原问题的解。
          // 减小或者复用程序调用系统栈: 将运算操做一次性执行完毕,而后再执行子函数。
          return this.tailSum(array, cur + 1, result + array[cur]);
       }
    }
    class Main {
       constructor() {
          this.alterLine('递归求和');
          let calc = new Calc();
          let arr = [1, 2, 3, 4];
    
          let arrInfo = `[`;
          for (var i = 0; i < arr.length - 1; i++) {
             arrInfo += `${arr[i]},`;
          }
          arrInfo += `${arr[arr.length - 1]}`;
          arrInfo += `]`;
          document.body.innerHTML += `${arrInfo}<br /><br />`;
          this.show(calc.sum(arr));
          this.show(calc.tailSum(arr));
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码
  2. 对于一个复杂的递归算法来讲,

    1. 这个逻辑多是很是复杂的,
    2. 虽说转化问题是一个难点,
    3. 实际上也并非那么难,
    4. 只不过不少在写逻辑的时候把本身给绕晕了,
    5. 函数本身调用本身,没必要过于纠结这里面程序执行的机制。
  3. 写递归函数的时候必定要注重递归函数自己的语意,

    1. 例如上面的 sum 函数,
    2. 它就是用来计算一个数组从索引 cur 开始
    3. 到最后一个位置之间全部的数字之和,
    4. 这个就是此递归函数的“宏观”语意
    5. 在这样的一个语意下,
    6. 在涉及转换逻辑的时候你要抛弃掉这是一个递归算法的这样的想法,
    7. 递归函数自己它也是一个函数,每一个函数其实就是完成一个功能。
    8. 在函数 A 中调函数 B 你并不会晕,可是在函数 A 里调用函数 A,
    9. 也就是在递归函数中你可能就会晕,
    10. 其实这和函数 A 里调用函数 B 在本质上并无区别。
  4. 你能够当这是一个子逻辑,这个子逻辑里面须要传两个参数,

    1. 它作的事情就是求数组里的从索引 cur 开始
    2. 到最后一个位置之间全部的数字之和,
    3. 你就仅仅是在用这个函数,至于或者函数是否是当前函数不必在乎,
    4. 其实就是这么简单的一件事情。
  5. 在写递归算法的时候,

    1. 有些时候不须要你特别微观的
    2. 陷进递归调用里的去纠结这个递归是怎么样调用的,
    3. 其实你能够直接把这个递归函数当成一个子函数,
    4. 这个子函数能够完成特定的功能,
    5. 而你要干的事情就是利用这个子函数来构建你本身的逻辑,
    6. 来解决上层的这一个问题就行了。
  6. 注意递归函数的宏观语意

    1. 把你要调用的递归函数看成是一个子函数或者子逻辑或者子过程,
    2. 而后去想这个子过程若是去帮你解决如今的这个问题就 ok,
    3. 要想熟练的掌握就须要大量的练习。

链表的自然递归结构性质

  1. 递归求解 203 号问题

    class Solution {
       // leetcode 203. 移除链表元素
       removeElements(head, val) {
          /** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */
          /** * @param {ListNode} head * @param {number} val * @return {ListNode} */
    
          // 递归求解三种方式
          var removeElements = function(head, val) {
             // 解决最基本的问题
             if (head === null) {
                return null;
             }
    
             // 第一种解决方式
             // let node = removeElements(head.next, val);
    
             // if (head.val === val) {
             // head = node;
             // } else {
             // head.next = node;
             // }
    
             // return head;
    
             // 第二种解决方式
             // if (head.val === val) {
             // head = removeElements(head.next, val);
             // } else {
             // head.next = removeElements(head.next, val);
             // }
             // return head;
    
             // 第三种方式
             head.next = removeElements(head.next, val);
             if (head.val === val) {
                return head.next;
             } else {
                return head;
             }
          };
    
          // 尾递归的方式 失败 没有到达那个程度
          // var removeElements = function(head, val, node = null) {
          // if (head === null) {
          // return node;
          // }
    
          // return removeElements(head.next, val , node = head);
    
          // }
    
          return removeElements(head, val);
       }
    }
    class Main {
       constructor() {
          this.alterLine('leetcode 203. 删除指定元素的全部节点(递归)');
          let s = new Solution();
    
          let arr = [1, 2, 3, 5, 1, 2, 1, 3, 5, 3, 5, 6, 3, 1, 5, 1, 3];
          let node = new ListNode(null);
          node.appendToLinkedListNode(arr);
    
          console.log(node.toString());
          let result = s.removeElements(node, 2);
          console.log(result.toString());
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

递归运行的机制及递归的“微观”解读

  1. 虽然写递归函数与递归算法时要注意递归算法的宏观语意,
    1. 可是站在一个更高的层面去思考这个函数它自己的功能做用是什么,
    2. 也许能够帮助你更好的完成整个递归的逻辑,
    3. 可是从另一方面递归函数想,递归函数究竟是怎样运转的,
    4. 它内部的机制是怎样的,因此递归的运行机制也是须要了解的。
  2. 经过数组求和与删除链表节点的递归实现来具体的观察递归的运行机制
    1. 栈的应用中说过程序调用的系统栈,
    2. 子函数调用的位置会压入到一个系统栈,
    3. 当子函数调用完成的时候,
    4. 程序就会从系统栈中找到上次父函数调用子函数的这个位置,
    5. 而后再父函数中的子函数这个位置后续继续执行,
    6. 其实递归调用和子函数子过程的调用没有区别,
    7. 只不过递归调用的函数仍是这个函数自己而已
    8. (本身调用本身,根据某些条件终止调用本身)。
  3. 递归的调用和子过程的调用是没有区别的
    1. 就像程序调用的系统栈同样。
    2. 父函数调用子函数,子函数执行完毕以后,
    3. 就会返回到上一层,也就是继续执行父函数,
    4. 这个执行并非从新执行,
    5. 而是从以前那个子函数调用时的位置继续往下执行,
    6. 若是子函数有返回值,那么接收一下也能够,
    7. 接收完了以后继续往下执行。
    A0();
       function A0 () {
          ...
          A1();
          ...
       }
       function A1 () {
          ...
          A2();
          ...
       }
       function A2 () {
          ...
          ...
          ...
       }
    复制代码
  4. 递归的调用时有代价的
    1. 函数调用 + 系统栈空间。
    2. 好比系统栈中会记录这些函数调用的信息,
    3. 如当前这个函数执行到哪儿了,
    4. 当前的局部变量都是怎样的一个状态,
    5. 而后给它压入系统栈。,
    6. 包括函数调用的自己在计算机的底层找到这个新的函数所在的位置,
    7. 这些都是有必定的时间消耗的。
    8. 递归调用的过程是很消耗系统栈的空间的,
    9. 若是递归函数中没有处理那个最基本的状况,
    10. 那么递归将一直执行下去,不会正常终止,
    11. 最终终止的结果确定就是异常报错,
    12. 由于系统栈占满了,空间不足。
    13. 在线性的调用过程当中,
    14. 当你递归的次数达到十万百万级别的话,
    15. 系统占仍是会被占满,由于存储了太多函数调用的状态信息。
  5. 用递归书写逻辑实际上是更简单的
    1. 这一点在线性的结构中看不出来,
    2. 这是由于线性的结构很是好想,
    3. 直接写循环就能解决全部的线性问题,
    4. 可是一旦进入非线性的结构 如树、图,
    5. 不少问题其实使用递归的方式解决将更加的简单。

数组求和的递归解析

  1. 原函数

    // 计算 arr[cur...n] 这个区间内的全部数字之和。
    sum (arr, cur = 0) {
       // 这个地方就是求解最基本问题
       // 一般对于递归算法来讲,
       // 最基本的问题就是极其简单的,
       // 基本上都是这样的一种形式
       // 由于最基本的问题太过于平凡了
       // 一眼就看出来那个答案是多少了
       if (arr.length === cur) {
          return 0;
       }
       // 这部分就是递归算法f最核心的部分
       // 把原问题转化成更小的问题的一个过程
       // 这个过程是难的,
       // 这个转化为更小的问题并不简单的求一个更小的问题的答案就行了,
       // 而是要根据这个更小的问题的答案构建出原问题的答案,
       // 这个构建 在这里就是一个加法的过程。
       return arr[cur] + this.sum(arr, cur + 1);
    }
    复制代码
  2. 解析原函数

    1. 递归函数的调用,本质就是就是函数调用,
    2. 只不过调用的函数就是本身而已。
    // 计算 arr[cur...n] 这个区间内的全部数字之和。
    sum (arr, cur = 0) {
    
          if (arr.length === cur) {
                return 0;
          }
    
          temp = sum(arr, cur + 1);
          result = arr[cur] + temp;
          return result;
    }
    复制代码
  3. 原函数解析 2

    1. 在 sum 函数中调用到了 sum 函数,
    2. 其实是在一个新的 sum 函数中 调用逻辑,
    3. 原来的 sum 函数中全部的变量保持不变,
    4. 等新的 sum 函数执行完了逻辑,
    5. 还会回到原来的 sum 函数中继续执行余下的逻辑。
    // 计算 arr[cur...n] 这个区间内的全部数字之和。
    
    // 代号 001
    // 使用 arr = [6, 10]
    // 调用 sum(arr, 0)
    sum (arr, cur = 0) {
    
       if (cur == n) return 0; // n 为数组的长度:2
    
       temp = sum(arr, cur + 1); // cur 为 0
       result = arr[cur] + temp;
       return result;
    }
    
    // 代号 002
    // 到了 上面的sum(arr, cur + 1)时
    // 实际 调用了 sum(arr, 1)
    sum (arr, cur = 0) {
    
       if (cur == n) return 0; // n 为数组的长度:2
    
       temp = sum(arr, cur + 1); // cur 为 1
       result = arr[cur] + temp;
       return result;
    }
    
    // 代号 003
    // 到了 上面的sum(arr, cur + 1)时
    // 实际 调用了 sum(arr, 2)
    sum (arr, cur = 0) {
    
       // n 为数组的长度:2,cur 也为:2
       // 因此sum函数到这里就终止了
       if (cur == n) return 0;
    
       temp = sum(arr, cur + 1); // cur 为 2
       result = arr[cur] + temp;
       return result;
    }
    
    // 上面的代号003的sum函数执行完毕后 返回 0。
    //
    // 那么 上面的代号002的sum函数中
    // temp = sum(arr, cur + 1),temp获取到的值 就为 0,
    // 而后继续执行代号002的sum函数里temp获取值时中断的位置 下面的逻辑,
    // 执行到了result = arr[cur] + temp,
    // temp为 0,cur 为 1,arr[1] 为 10,因此result 为 0 + 10 = 10,
    // 这样一来 代号002的sum函数执行完毕了,返回 10。
    //
    // 那么 代号001的sum函数中
    // temp = sum(arr, cur + 1),temp获取到的值 就为 10,
    // 而后继续执行代号001的sum函数里temp获取值时中断的位置 下面的逻辑,
    // 执行到了result = arr[cur] + temp,
    // temp为 10,cur 为 0,arr[0] 为 6,因此result 为 6 + 10 = 16,
    // 这样一来 代号001的sum函数执行完毕了,返回 16。
    //
    // 代号001的sum函数没有被其它代号00x的sum函数调用,
    // 因此数组求和的最终结果就是 16。
    复制代码
  4. 调试递归函数的思路

    1. 若是对递归函数运转的机制不理解,
    2. 不要对着递归函数去生看生想,
    3. 在不少时候你确定会越想越乱,
    4. 不如你用一个很是小的测试数据集直接扔进这个函数中,
    5. 你可使用纸笔画或者使用 IDE 提供的 debug 工具,
    6. 一步一步的去看这个程序在每一步执行后计算的结果是什么,
    7. 一般使用这种方式可以帮助你更加清晰的理解程序的运转逻辑,
    8. 计算机是一门工科,和工程相关的科学,
    9. 工程相关的科学虽然也注重理论它背后也有理论支撑,
    10. 可是从工程的角度入手来实践是很是很是重要的,
    11. 不少时候你若是想理解它背后的理论,
    12. 可能更好的方式不是去想这个理论,
    13. 而是实际的去实践一下看看这个过程究竟是怎么回事儿。

删除链表节点的递归解析

  1. 原函数

    var removeElements = function(head, val) {
       if (head == null) {
          return null;
       }
    
       head.next = removeElements(head.next, val);
       if (head.val == val) {
          return head.next;
       } else {
          return head;
       }
    };
    复制代码
  2. 解析原函数

    1. 递归调用的时候就是子过程的调用,
    2. 一步一步的向下调用,调用完毕以后,
    3. 子过程计算出结果后再一步一步的返回给上层的调用,
    4. 最终获得了最终的结果,6->7->8->null 删除掉 7 以后就是 6->8->null,
    5. 节点真正的删除是发生在步骤 3 中,
    6. 在使用解决了一个更小规模的问题相应的解以后,
    7. 结合当前的调用,组织逻辑,组织出了当前这个问题的解,
    8. 就是这样的一个过程。
    // 操做函数编号 001
    var removeElements = function(head, val) {
       // head:6->7->8->null
       //步骤1
       if (head == null) return null;
    
       //步骤2
       head.next = removeElements(head.next, val);
       //步骤3
       return head.val == val ? head.next : head;
    };
    // 模拟调用,对 6->7->8->null 进行7的删除
    // 调用 removeElments(head, 7);
    // 执行步骤1,head当前的节点为6,既然不为null,因此不返回null,
    // 继续执行步骤2,head.next = removeElements(head.next, 7),
    // 求当前节点后面的一个节点,后面的一个节点目前不知道,
    // 可是能够经过removeElements(head.next, 7)这样的子过程调用求出来,
    // 此次传入的是当前节点的next,也就是7的这个节点,7->8->null。
    
    // 操做函数编号 002
    var removeElements = function(head, val) {
       // head:7->8->null
       //步骤1
       if (head == null) return null;
    
       //步骤2
       head.next = removeElements(head.next, val);
       //步骤3
       return head.val == val ? head.next : head;
    };
    // 模拟调用,对 7->8->null 进行7的删除
    // 调用 removeElements(head.next, 7);
    // head.next 会被赋值给 函数中的局部变量 head,
    // 也就是调用时被转换为 removeElements(head, 7);
    // 执行步骤1,head当前的节点为7,不为null,因此也不会返回null,
    // 继续执行步骤2,head.next = removeElements(head.next, 7),
    // 求当前节点后面的一个节点,后面的一个节点目前不知道,
    // 可是能够经过removeElements(head.next, 7)这样的子过程调用求出来,
    // 此次传入的也是当前节点的next,也就是8的这个节点,8->null。
    
    // 操做函数编号 003
    var removeElements = function(head, val) {
       // head:8->null
       // 步骤1
       if (head == null) return null;
    
       // 步骤2
       head.next = removeElements(head.next, val);
       // 步骤3
       return head.val == val ? head.next : head;
    };
    // 模拟调用,对 8->null 进行7的删除
    // 调用 removeElements(head.next, 7);
    // head.next 会被赋值给 函数中的局部变量 head,
    // 也就是调用时被转换为 removeElements(head, 7);
    // 执行步骤1,head当前的节点为7,不为null,因此也不会返回null,
    // 继续执行步骤2,head.next = removeElements(head.next, 7),
    // 求当前节点后面的一个节点,后面的一个节点目前不知道,
    // 可是能够经过removeElements(head.next, 7)这样的子过程调用求出来,
    // 此次传入的也是当前节点的next,也就是null的这个节点,null。
    
    // 操做函数编号 004
    var removeElements = function(head, val) {
       // head:null
       // 步骤1
       if (head == null) return null;
    
       // 步骤2
       head.next = removeElements(head.next, val);
       // 步骤3
       return head.val == val ? head.next : head;
    };
    // 模拟调用,对 null 进行7的删除
    // 调用 removeElements(head.next, 7);
    // head.next 会被赋值给 函数中的局部变量 head,
    // 也就是调用时被转换为 removeElements(head, 7);
    // 执行步骤1,head当前的节点为null,直接返回null,不继续向下执行了。
    
    // 操做函数编号 003
    var removeElements = function(head, val) {
       // head:8->null
       //步骤1
       if (head == null) return null;
    
       //步骤2
       head.next = removeElements(head.next, val);
       //步骤3
       return head.val == val ? head.next : head;
    };
    // 这时候回到操做函数编号 004的上一层中来,
    // 操做函数编号 003 调用到了步骤2,而且head.next接收到的返回值为null,
    // 继续操做函数编号 003 的步骤3,判断当前节点的val是否为7,
    // 很明显函数编号003里的当前节点的val为8,因此返回当前的节点 8->null。
    
    // 操做函数编号 002
    var removeElements = function(head, val) {
       // head:7->8->null
       //步骤1
       if (head == null) return null;
    
       //步骤2
       head.next = removeElements(head.next, val);
       //步骤3
       return head.val == val ? head.next : head;
    };
    // 这时候回到操做函数编号 003的上一层中来,
    // 操做函数编号 002 调用到了步骤2,head.next接收到的返回值为节点 8->null,
    // 继续操做函数编号 002 的步骤3,判断当前节点的val是否为7,
    // 此时函数编号 002 的当前节点的val为7,因此返回就是当前节点的next 8->null,
    // 也就是说不返回当前的节点 head:7->8->null ,改返回当前节点的下一个节点,
    // 这样一来就至关于删除了当前这个节点,改让父节点的next指向当前节点的next。
    
    // 操做函数编号 001
    var removeElements = function(head, val) {
       // head:6->7->8->null
       //步骤1
       if (head == null) return null;
    
       //步骤2
       head.next = removeElements(head.next, val);
       //步骤3
       return head.val == val ? head.next : head;
    };
    // 这时候回到操做函数编号 002的上一层中来,
    // 操做函数编号 001 调用到了步骤2,head.next接收到的返回值为节点 8->null,
    // 继续操做函数编号 001 的步骤3,判断当前节点的val是否为7,
    // 函数编号 001 中当前节点的val为6,因此返回当前的节点 head:6->8->null,
    // 以前当前节点 为head:6->7->8->null,因为head.next在步骤2时发生了改变,
    // 原来老的head.next(head:7->8->null) 从链表中剔除了,
    // 因此当前节点 为head:6->8->null。
    
    // 链表中包含节点的val为7的节点都被剔除,操做完毕。
    复制代码

递归算法的调试

  1. 能够以动画的方式展现递归函数底层的运行机理,
    1. 一帧一帧的动画来展现递归函数的具体执行过程。
  2. 可是在实际调试递归函数时
    1. 很难画出那么详细的动画,相对也比较费时间,
    2. 可是也能够拿一张 A4 的白纸仔细的一下,
    3. 例如 画一个比较小的测试用例的执行过程是怎样的,
    4. 这样对于理解你的程序或者找出你的程序中有错误,
    5. 是很是有帮助的
  3. 调试方法
    1. 靠打印输出,
    2. 彻底可使用打印输出的方式
    3. 清楚的看出程序在执行过程当中是怎样一步一步得到最终结果。
    4. 单步跟踪,
    5. 也就是每个 IDE 中自带的调试功能。
    6. 视状况来定。
  4. 对于递归函数来讲有一个很是重要的概念
    1. 递归的深度,
    2. 每个函数在本身的内部都会去调用了一下本身,
    3. 那么就表明每次调用本身时,整个递归的深度就多了 1,
    4. 因此在具体的输出可视化这个递归函数时,
    5. 这个递归深度是能够帮助你理解这个递归过程的一个变量,
    6. 在原递归函数中新增一个参数depth
    7. 根据这个变量生成递归深度字符串--
    8. --相同则表明同一递归深度。
  5. 不少时候要想真正理解一个算法或者理解一个函数
    1. 其实并无什么捷径,确定是要费一些劲,
    2. 若是你不想在纸上画出来的话,
    3. 那么你就要用代码画出来,
    4. 也就是要在代码上添加不少的辅助代码,
    5. 这就是平时去理解程序或作练习时不要去犯懒,
    6. 可能只要写 4 行代码就能解决问题,
    7. 可是这背后颇有多是你写了
    8. 几十行甚至上百行的代码
    9. 最终终于透彻的理解了这个程序,
    10. 而后才能潇洒的用四行代码来解决这个问题。
  6. 不停的练习如何写一个递归的函数,才能理解理解这个递归的过程。

代码示例 (class: ListNode, class: Solution)

  1. ListNode

    class ListNode {
       constructor(val) {
          this.val = val;
          this.next = null;
       }
    
       // 将一个数组对象 转换为一个链表 而且追加到当前节点上
       appendToLinkedListNode(array) {
          let head = null;
          if (this.val === null) {
             // 头部添加
             head = this;
             head.val = array[0];
             head.next = null;
          } else {
             // 插入式
             head = new ListNode(array[0]);
             head.next = this.next;
             this.next = head;
          }
    
          // 添加节点的方式 头部添加、尾部添加、中间插入
    
          // 尾部添加节点的方式
          for (var i = 1; i < array.length; i++) {
             head.next = new ListNode(array[i]);
             head = head.next;
          }
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       // toString () {
       // let arrInfo = `ListNode: \n`;
       // arrInfo += `data = front [`;
       // let node = this;
       // while (node.next !== null) {
       // arrInfo += `${node.val}->`;
       // node = node.next;
       // }
       // arrInfo += `${node.val}->`;
       // arrInfo += "NULL] tail";
    
       // // 在页面上展现
       // document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
       // return arrInfo;
       // }
    
       toString() {
          let arrInfo = `ListNode = `;
          arrInfo += `front [`;
          let node = this;
          while (node.next !== null) {
             arrInfo += `${node.val}->`;
             node = node.next;
          }
          arrInfo += `${node.val}->`;
          arrInfo += 'NULL] tail';
    
          return arrInfo;
       }
    }
    复制代码
  2. Solution

    class Solution {
       // leetcode 203. 移除链表元素
       removeElements(head, val) {
          /** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */
          /** * @param {ListNode} head * @param {number} val * @return {ListNode} */
          // 深刻理解递归过程
          var removeElements = function(head, val, depth = 0) {
             // 首次输出 开始调用函数
             let depthString = generateDepathString(depth);
             let info = depthString + 'Call: remove ' + val + ' in ' + head;
             show(info);
    
             if (head === null) {
                // 第二次输出 解决最基本的问题时
                info = depthString + 'Return :' + head;
                show(info);
    
                return null;
             }
    
             let result = removeElements(head.next, val, depth + 1);
    
             // 第三次输出 将原问题分解为小问题
             info = depthString + 'After: remove ' + val + ' :' + result;
             show(info);
    
             let ret = null;
             if (head.val === val) {
                ret = result;
             } else {
                head.next = result;
                ret = head;
             }
    
             // 第四次输出 求出小问题的解
             info = depthString + 'Return :' + ret;
             show(info);
    
             return ret;
          };
    
          // 辅助函数 生成递归深度字符串
          function generateDepathString(depth) {
             let arrInfo = ``;
             for (var i = 0; i < depth; i++) {
                arrInfo += `-- `; // -- 表示深度,--相同则表明在同一递归深度
             }
             return arrInfo;
          }
    
          // 辅助函数 输出内容 到页面和控制台上
          function show(content) {
             document.body.innerHTML += `${content}<br /><br />`;
             console.log(content);
          }
    
          return removeElements(head, val);
       }
    }
    class Main {
       constructor() {
          this.alterLine('leetcode 203. 删除指定元素的全部节点(递归) 调试');
          let s = new Solution();
    
          let arr = [1, 2, 3];
          let node = new ListNode(null);
          node.appendToLinkedListNode(arr);
          this.show(node);
          s.removeElements(node, 2);
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    复制代码

更多与链表相关的问题

  1. 关于递归
    1. 链表有自然递归性质结构
  2. 几乎和链表相关的全部操做
    1. 均可以使用递归的形式完成
  3. 练习时能够对链表的增删改查进行递归实现
    1. 以前链表的增删改查使用了循环的方式进行了实现,
    2. 如今能够对链表的增删改为进行递归的方式实现,
    3. 这个练习是很是有意义的,可以帮助你更好的理解递归。
    4. 虽然实际使用链表时是不须要使用递归的,
    5. 可是进行一下这种练习可让你更好的对递归有更深入的理解。
  4. 其它和链表相关的题目能够到 leetcode 上查找
    1. 链表:https://leetcode-cn.com/tag/linked-list/
    2. 不要完美主义,不要想着把这些问题一会儿所有作出来,
    3. 根据本身的实际状况来制定计划,在本身处于什么样的水平的时候,
    4. 完成怎样的问题,可是这些问题一直都会在 leetcode 上,
    5. 慢慢来,一点一点的实现。
  5. 关于链表的技术研究,由斯坦福提出的问题研究
    1. 文章地址:https://max.book118.com/html/2017/0902/131359982.shtm
    2. 都看懂了,那你就彻底的懂了链表。
  6. 非线性数据结构
    1. 如大名鼎鼎的二分搜索树
    2. 二分搜索树也是一个动态的数据结构
    3. 也是靠节点挂接起来的,
    4. 只不过那些节点没有排成一根线,
    5. 而是排成了一颗树,
    6. 不是每个节点有指向下一个节点的 next,
    7. 而是由指向左子树和右子树的两个根节点而已。

双链表

  1. 对于队列来讲须要对链表的两端进行操做
    1. 在两端进行操做的时候就遇到了问题,
    2. 在尾端删除元素,
    3. 即便在尾端有 tail 的变量指向链表的结尾,
    4. 它依然是一个O(n)复杂度的,。
    5. 对于这个问题其实有一个解决方案,
    6. 这个问题的解决方案就是双链表,
  2. 所谓的双链表就是在链表中每个节点包含两个指针
    1. 指针就表明着引用,
    2. 有一个变量 next 指向这个节点的下一个节点,
    3. 有一个变量 prev 指向这个节点的前一个节点,
    4. 对于双链表来讲,
    5. 你有了 tail 这个节点以后,
    6. 删除尾部的节点就很是简单,
    7. 并且这个操做会是O(1)级别的,
    8. 可是代价是每个节点从原来只有一个指针变为两个指针,
    9. 那么维护起来就会相对复杂一些。
    class Node {
       e; // Element
       next; //Node
       prev; //Node
    }
    复制代码

循环链表

  1. 对于循环链表来讲,也可使用双链表的思路来实现,
    1. 不过须要设立一个虚拟的头节点,
    2. 从而让整个链表造成了一个环,
    3. 这里面最重要的是 尾节点不指向空而是指向虚拟头节点,
    4. 能够很方便的判断某一个节点的下一个节点是不是虚拟头节点
    5. 来肯定这个节点是否是尾节点,
    6. 循环链表的好处是 进一步的把不少操做进行了统一,
    7. 好比说在链表结尾添加元素只须要在 dummyHead 的前面
    8. 添加要一个给元素,它就等因而在整个链表的结尾添加了一个元素,
    9. 事实上循环链表自己是很是有用的,
    10. 强类型语言 Java 中的 LinkedList 类底层的实现,本质就是一个循环链表,
    11. 更准确一些,就是循环双向链表,由于每一个节点是有两个指针的。

链表也是使用数组来实现

  1. 由于链表的 next 只是指向下一个元素,
  2. 在数组中每个位置存储的不只仅是有这个元素,
  3. 再多存储一个指向下一个元素的索引,
  4. 这个链表中每个节点是这样的,
  5. Node 类中有一个 int 的变量 next 指向下一个元素的索引,
  6. 在有一些状况下,好比你明确的知道你要处理的元素有多少个,
  7. 这种时候使用这种数组链表有多是更加方便的。
class Node {
   e; // Element
   next; //int
}
复制代码
相关文章
相关标签/搜索