【从蛋壳到满天飞】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
(class: Solution)
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);
}
}
复制代码
(class: ListNode, class: Solution,
数组
class: Solution2, class: Main)
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;
}
}
复制代码
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;
};
}
复制代码
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);
}
}
复制代码
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();
};
复制代码
数组求和:求数组中 n 个元素的和
Sum(arr[0...n-1]) = arr[0] + Sum(arr[1...n-1])
第一次,Sum(arr[1...n-1]) = arr[1] + Sum(arr[2...n-1])
第二次,...
若干次Sum(arr[n-1...n-1]) = arr[n-1] + Sum(arr[])
最后一次`if (arr.length == cur) {return 0;}
就是解决最基本的问题arr[cur] + sum(arr, cur+1);
就是在构建原问题的答案sum(arr, cur+1);
就是不断的将原问题转化为更小的问题,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();
};
复制代码
对于一个复杂的递归算法来讲,
写递归函数的时候必定要注重递归函数自己的语意,
“宏观”语意
,你能够当这是一个子逻辑,这个子逻辑里面须要传两个参数,
在写递归算法的时候,
注意递归函数的宏观语意
递归求解 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();
};
复制代码
A0();
function A0 () {
...
A1();
...
}
function A1 () {
...
A2();
...
}
function A2 () {
...
...
...
}
复制代码
原函数
// 计算 arr[cur...n] 这个区间内的全部数字之和。
sum (arr, cur = 0) {
// 这个地方就是求解最基本问题
// 一般对于递归算法来讲,
// 最基本的问题就是极其简单的,
// 基本上都是这样的一种形式
// 由于最基本的问题太过于平凡了
// 一眼就看出来那个答案是多少了
if (arr.length === cur) {
return 0;
}
// 这部分就是递归算法f最核心的部分
// 把原问题转化成更小的问题的一个过程
// 这个过程是难的,
// 这个转化为更小的问题并不简单的求一个更小的问题的答案就行了,
// 而是要根据这个更小的问题的答案构建出原问题的答案,
// 这个构建 在这里就是一个加法的过程。
return arr[cur] + this.sum(arr, cur + 1);
}
复制代码
解析原函数
// 计算 arr[cur...n] 这个区间内的全部数字之和。
sum (arr, cur = 0) {
if (arr.length === cur) {
return 0;
}
temp = sum(arr, cur + 1);
result = arr[cur] + temp;
return result;
}
复制代码
原函数解析 2
// 计算 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。
复制代码
调试递归函数的思路
原函数
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;
}
};
复制代码
解析原函数
// 操做函数编号 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的节点都被剔除,操做完毕。
复制代码
depth
,--
,--
相同则表明同一递归深度。(class: ListNode, class: Solution)
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;
}
}
复制代码
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 />`;
}
}
复制代码
https://leetcode-cn.com/tag/linked-list/
,https://max.book118.com/html/2017/0902/131359982.shtm
,O(n)
复杂度的,。O(1)
级别的,class Node {
e; // Element
next; //Node
prev; //Node
}
复制代码
class Node {
e; // Element
next; //int
}
复制代码