【從蛋殼到滿天飛】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
}
複製代碼