導師計劃已經開始一個月了,本身的講解的課程選擇了數據結構和算法。這個系列的講解分爲上下兩章
,javascript
語言輔助。本篇文章爲上章,涉及的內容是基本的數據結構。在日本,晚上沒事安排@…@,時間仍是充足的...,因而本身整理下本系列知識點的上章內容。javascript
如下爲正文:前端
數據結構是計算機存儲、組織數據的方式。數據結構是指相互直接存在一種或多種特殊關係的數據元素的集合。一般狀況下,精心選擇數據結構能夠帶來更高的運行或者存儲效率。做爲一名程序猿,更須要了解下數據結構。AND WHY?能夠參考這篇文章【譯】編程不容易中的性能和優化
部份內容。java
講到數據結構,咱們都會談到線性結構和非線性結構。node
1.線性結構是一個有序數據元素的集合。它應該知足下面的特徵:git
按照百度百科的定義,咱們知道符合條件的數據結構就有棧、隊列和其它。es6
2.非線性結構其邏輯特徵是一個節點元素能夠有多個直接前驅或多個直接後繼。github
那麼,符合條件的數據結構就有圖、樹和其它。算法
嗯~瞭解一下就行。咱們進入正題:編程
數組是一種線性結構,以十二生肖(鼠、牛、虎、兔、龍、蛇、馬、羊、猴、雞、狗、豬)排序爲例:數組
咱們來建立一個數組並打印出結果就一目瞭然了:
let arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'];
arr.forEach((item, index) => {
console.log(`[ ${index} ] => ${item}`);
});
// [ 0 ] => 鼠
// [ 1 ] => 牛
// [ 2 ] => 虎
// [ 3 ] => 兔
// [ 4 ] => 龍
// [ 5 ] => 蛇
// [ 6 ] => 馬
// [ 7 ] => 羊
// [ 8 ] => 猴
// [ 9 ] => 雞
// [ 10 ] => 狗
// [ 11 ] => 豬
複製代碼
數組中經常使用的屬性和一些方法以下,直接調用相關的方法便可。這裏不作演示~
經常使用的屬性
經常使用的方法
splice(index, howmany, item, ... itemx)
splice方法自認爲是數組中最強大的方法。能夠實現數組元素的添加、刪除和替換。參數index
爲整數且必需,規定添加/刪除項目的位置,使用負數可從數組結尾處規定位置;參數howmany
爲必需,爲要刪除的項目數量,若是設置爲 0,則不會刪除項目;item1, ... itemx
爲可選,向數組添加新的項目。
indexOf(searchValue, fromIndex)
indexOf方法返回某個指定字符串值在數組中的位置。searchValue
是查詢的字符串;fromIndex
是查詢的開始位置,默認是0。若是查詢不到,會返回-1。
concat(array1, ... arrayn)
concat方法用於鏈接兩個或者多個數組。
push(newElement1, ... newElementN)
push方法可向數組的末尾
添加一個或者多個元素。
unshift(newElement1, ... newElementN)
unshift方法可向數組的開頭
添加一個或者多個元素。
pop()
pop方法用於刪除並返回數組的最後一個元素
。
shift()
shift方法能夠刪除數組的第一個元素
。
reverse()
reverse方法用於數組的反轉
sort(sortFn)
sort方法是對數組的元素排序。參數sortFn
可選,其規定排序順序,必須是函數。
let values = [0, 1, 5, 10, 15];
values.sort();
console.log(values); // [0, 1, 10, 15, 5]
// 爲何會出現這種排序結果呢❓
// 由於在忽略sortFn的狀況下,元素會按照轉換爲字符串的各個字符的Unicode位點進行排序,以下
let equalValues = ['0', '1', '5', '10', '15'];
equalValues.sort();
console.log(equalValues); // ["0", "1", "10", "15", "5"]
let arr = [0, 10, 5, 1, 15];
function compare(el1, el2){
return el1 - el2; // 升序排列
}
arr.sort(compare);
console.log(arr); // [0, 1, 5, 10, 15]
arr.sort((el1, el2) => {
return el2 - el1; // 降序排列
});
console.log(arr); // [15, 10, 5, 1, 0]
複製代碼
forEach(fn(currentValue, index, arr), thisValue)
forEach方法用於調用數組的每一個元素,並將元素傳遞給回調函數。參數function(currentValue, index, arr){}
是一個回調函數。thisValue
可選,傳遞給函數的值通常用 "this" 值,若是這個參數爲空, "undefined" 會傳遞給 "this" 值。
every(fn(currentValue, index, arr), thisValue)
every方法用於檢測數組中全部元素是否符合指定條件,若是數組中檢測到有一個元素不知足,則整個表達式返回false
,且剩餘的元素再也不檢查。若是全部的元素都知足條件,則返回true
。
some(fn(currentValue,index,arr),thisValue)
some方法用於檢測數組中元素是否知足指定條件。只要有一個符合就返回true
,剩餘的元素再也不檢查。若是全部元素都不符合條件,則返回false
。
reduce(fn(accumulator, currentValue, currentIndex, arr), initialValue)
reduce方法接收一個函數做爲累加器,數組中的每一個值(從左到右)開始縮減,最終爲一個值。回調函數的四個參數的意義以下:accumulator
,必需,累計器累計回調的返回值, 它是上一次調用回調時返回的累積值,或initialValue;currentValue
,必需,數組中正在處理的元素;currentIndex
,可選,數組中正在處理的當前元素的索引,若是提供了initialValue,則起始索引號爲0,不然爲1;arr
,可選,當前元素所屬的數組對象。initialValue
,可選,傳遞給函數的初始值。
let arr = [1, 2, 3, 4];
let reducer = (accumulator, currentValue) => accumulator + currentValue;
// 1 + 2 + 3 + 4
console.log(arr.reduce(reducer)); // 10
// 5 + 1 + 2 + 3 + 4
console.log(arr.reduce(reducer, 5)); // 15
複製代碼
棧是一種後進先出(LIFO)線性表,是一種基於數組的數據結構。(ps:其實後面講到的數據結構或多或少有數組的影子)
咱們代碼寫下,熟悉下棧:
class Stack {
constructor(){
this.items = [];
}
// 入棧操做
push(element = ''){
if(!element) return;
this.items.push(element);
return this;
}
// 出棧操做
pop(){
this.items.pop();
return this;
}
// 對棧一瞥,理論上只能看到棧頂或者說即將處理的元素
peek(){
return this.items[this.size() - 1];
}
// 打印棧數據
print(){
return this.items.join(' ');
}
// 棧是否爲空
isEmpty(){
return this.items.length == 0;
}
// 返回棧的元素個數
size(){
return this.items.length;
}
}
let stack = new Stack(),
arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'];
arr.forEach(item => {
stack.push(item);
});
console.log(stack.print()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(stack.peek()); // 豬
stack.pop().pop().pop().pop();
console.log(stack.print()); // 鼠 牛 虎 兔 龍 蛇 馬 羊
console.log(stack.isEmpty()); // false
console.log(stack.size()); // 8
複製代碼
⚠️ 注意:棧這裏的push和pop方法要和數組方法的push和pop方法區分下。
說到棧,這也讓我想到了翻譯的一篇文章JS的執行上下文和環境棧是什麼?,感興趣的話能夠戳進去看下。
隊列是一種先進先出(FIFO)受限的線性表。受限體如今於其容許在表的前端(front)進行刪除操做,在表的末尾(rear)進行插入【優先隊列這些排除在外】操做。
代碼走一遍:
class Queue {
constructor(){
this.items = [];
}
// 入隊操做
enqueue(element = ''){
if(!element) return;
this.items.push(element);
return this;
}
// 出隊操做
dequeue(){
this.items.shift();
return this;
}
// 查看隊前元素或者說即將處理的元素
front(){
return this.items[0];
}
// 查看隊列是否爲空
isEmpty(){
return this.items.length == 0;
}
// 查看隊列的長度
len(){
return this.items.length;
}
// 打印隊列數據
print(){
return this.items.join(' ');
}
}
let queue = new Queue(),
arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'];
arr.forEach(item => {
queue.enqueue(item);
});
console.log(queue.print()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(queue.isEmpty()); // false
console.log(queue.len()); // 12
queue.dequeue().dequeue();
console.log(queue.front()); // 虎
console.log(queue.print()); // 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
複製代碼
在進入正題以前,咱們先來聊聊數組的優缺點。
優勢:
缺點:
相對數組,鏈表亦能夠存儲多個元素,並且存儲的元素在內容中沒必要是連續的空間;在插入和刪除數據時,時間複雜度能夠達到O(1)。在查找元素的時候,仍是須要從頭開始遍歷的,比數組在知道下表的狀況下要快,可是數組若是不肯定下標的話,那就另說了...
咱們使用十二生肖來了解下鏈表:
鏈表是由一組節點組成的集合。每一個節點都使用一個對象的引用指向它的後繼。如上圖。下面用代碼實現下:
// 鏈表
class Node {
constructor(element){
this.element = element;
this.next = null;
}
}
class LinkedList {
constructor(){
this.length = 0; // 鏈表長度
this.head = new Node('head'); // 表頭節點
}
/** * @method find 查找元素的功能,找不到的狀況下直接返回鏈尾節點 * @param { String } item 要查找的元素 * @return { Object } 返回查找到的節點 */
find(item = ''){
let currNode = this.head;
while(currNode.element != item && currNode.next){
currNode = currNode.next;
}
return currNode;
}
/** * @method findPrevious 查找鏈表指定元素的前一個節點 * @param { String } item 指定的元素 * @return { Object } 返回查找到的以前元素的前一個節點,找不到節點的話返回鏈尾節點 */
findPrevious(item){
let currNode = this.head;
while((currNode.next != null) && (currNode.next.element != item)){
currNode = currNode.next;
}
return currNode;
}
/** * @method insert 插入功能 * @param { String } newElement 要出入的元素 * @param { String } item 想要追加在後的元素(此元素不必定存在) */
insert(newElement = '', item){
if(!newElement) return;
let newNode = new Node(newElement),
currNode = this.find(item);
newNode.next = currNode.next;
currNode.next = newNode;
this.length++;
return this;
}
// 展現鏈表元素
display(){
let currNode = this.head,
arr = [];
while(currNode.next != null){
arr.push(currNode.next.element);
currNode = currNode.next;
}
return arr.join(' ');
}
// 鏈表的長度
size(){
return this.length;
}
// 查看鏈表是否爲空
isEmpty(){
return this.length == 0;
}
/** * @method indexOf 查看鏈表中元素的索引 * @param { String } element 要查找的元素 */
indexOf(element){
let currNode = this.head,
index = 0;
while(currNode.next != null){
index++;
if(currNode.next.element == element){
return index;
}
currNode = currNode.next;
}
return -1;
}
/** * @method removeEl 移除指定的元素 * @param { String } element */
removeEl(element){
let preNode = this.findPrevious(element);
preNode.next = preNode.next != null ? preNode.next.next : null;
}
}
let linkedlist = new LinkedList();
console.log(linkedlist.isEmpty()); // true
linkedlist.insert('鼠').insert('虎').insert('牛', '鼠');
console.log(linkedlist.display()); // 鼠 牛 虎
console.log(linkedlist.find('豬')); // Node { element: '虎', next: null }
console.log(linkedlist.find('鼠')); // Node { element: '鼠', next: Node { element: '牛', next: Node { element: '虎', next: null } } }
console.log(linkedlist.size()); // 3
console.log(linkedlist.indexOf('鼠')); // 1
console.log(linkedlist.indexOf('豬')); // -1
console.log(linkedlist.findPrevious('虎')); // Node { element: '牛', next: Node { element: '虎', next: null } }
linkedlist.removeEl('鼠');
console.log(linkedlist.display()); // 牛 虎
複製代碼
字典的主要特色是鍵值一一對應的關係。能夠比喻成咱們現實學習中查不一樣語言翻譯的字典
。這裏字典的鍵(key)理論上是可使用任意的內容,但仍是建議語意化一點,好比下面的十二生肖圖:
class Dictionary {
constructor(){
this.items = {};
}
/** * @method set 設置字典的鍵值對 * @param { String } key 鍵 * @param {*} value 值 */
set(key = '', value = ''){
this.items[key] = value;
return this;
}
/** * @method get 獲取某個值 * @param { String } key 鍵 */
get(key = ''){
return this.has(key) ? this.items[key] : undefined;
}
/** * @method has 判斷是否含有某個鍵的值 * @param { String } key 鍵 */
has(key = ''){
return this.items.hasOwnProperty(key);
}
/** * @method remove 移除元素 * @param { String } key */
remove(key){
if(!this.has(key)) return false;
delete this.items[key];
return true;
}
// 展現字典的鍵
keys(){
return Object.keys(this.items).join(' ');
}
// 字典的大小
size(){
return Object.keys(this.items).length;
}
// 展現字典的值
values(){
return Object.values(this.items).join(' ');
}
// 清空字典
clear(){
this.items = {};
return this;
}
}
let dictionary = new Dictionary(),
// 這裏須要修改
arr = [{ key: 'mouse', value: '鼠'}, {key: 'ox', value: '牛'}, {key: 'tiger', value: '虎'}, {key: 'rabbit', value: '兔'}, {key: 'dragon', value: '龍'}, {key: 'snake', value: '蛇'}, {key: 'horse', value: '馬'}, {key: 'sheep', value: '羊'}, {key: 'monkey', value: '猴'}, {key: 'chicken', value: '雞'}, {key: 'dog', value: '狗'}, {key: 'pig', value: '豬'}];
arr.forEach(item => {
dictionary.set(item.key, item.value);
});
console.log(dictionary.keys()); // mouse ox tiger rabbit dragon snake horse sheep monkey chicken dog pig
console.log(dictionary.values()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(dictionary.has('dragon')); // true
console.log(dictionary.get('tiger')); // 虎
console.log(dictionary.remove('pig')); // true
console.log(dictionary.size()); // 11
console.log(dictionary.clear().size()); // 0
複製代碼
集合一般是由一組無序的,不能重複的元素構成。 一些常見的集合操做如圖:
es6中已經封裝好了可用的Set類。咱們手動來寫下相關的邏輯:
// 集合
class Set {
constructor(){
this.items = [];
}
/** * @method add 添加元素 * @param { String } element * @return { Boolean } */
add(element = ''){
if(this.items.indexOf(element) >= 0) return false;
this.items.push(element);
return true;
}
// 集合的大小
size(){
return this.items.length;
}
// 集合是否包含某指定元素
has(element = ''){
return this.items.indexOf(element) >= 0;
}
// 展現集合
show(){
return this.items.join(' ');
}
// 移除某個元素
remove(element){
let pos = this.items.indexOf(element);
if(pos < 0) return false;
this.items.splice(pos, 1);
return true;
}
/** * @method union 並集 * @param { Array } set 數組集合 * @return { Object } 返回並集的對象 */
union(set = []){
let tempSet = new Set();
for(let i = 0; i < this.items.length; i++){
tempSet.add(this.items[i]);
}
for(let i = 0; i < set.items.length; i++){
if(tempSet.has(set.items[i])) continue;
tempSet.items.push(set.items[i]);
}
return tempSet;
}
/** * @method intersect 交集 * @param { Array } set 數組集合 * @return { Object } 返回交集的對象 */
intersect(set = []){
let tempSet = new Set();
for(let i = 0; i < this.items.length; i++){
if(set.has(this.items[i])){
tempSet.add(this.items[i]);
}
}
return tempSet;
}
/** * @method isSubsetOf 【A】是【B】的子集❓ * @param { Array } set 數組集合 * @return { Boolean } 返回真假值 */
isSubsetOf(set = []){
if(this.size() > set.size()) return false;
this.items.forEach*(item => {
if(!set.has(item)) return false;
});
return true;
}
}
let set = new Set(),
arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴'];
arr.forEach(item => {
set.add(item);
});
console.log(set.show()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴
console.log(set.has('豬')); // false
console.log(set.size()); // 9
set.remove('鼠');
console.log(set.show()); // 牛 虎 兔 龍 蛇 馬 羊 猴
let setAnother = new Set(),
anotherArr = ['馬', '羊', '猴', '雞', '狗', '豬'];
anotherArr.forEach(item => {
setAnother.add(item);
});
console.log(set.union(setAnother).show()); // 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(set.intersect(setAnother).show()); // 馬 羊 猴
console.log(set.isSubsetOf(setAnother)); // false
複製代碼
散列是一種經常使用的存儲技術,散列使用的數據結構叫作散列表/哈希表。在散列表上插入、刪除和取用數據都很是快,可是對於查找操做來講卻效率低下,好比查找一組數據中的最大值和最小值。查找的這些操做得求助其它數據結構,好比下面要講的二叉樹。
切入個案例感覺下哈希表:
假如一家公司有1000個員工, 如今咱們須要將這些員工的信息使用某種數據結構來保存起來。你會採用什麼數據結構呢?
方案一:數組
編號
對應的就是員工的下標值
。方案二:鏈表
最終方案:
那麼散列表的原理和實現又是怎樣的呢,咱們來聊聊。
咱們的哈希表是基於數組完成的,咱們從數組這裏切入解析下。數組能夠經過下標直接定位到相應的空間
,哈希表的作法就是相似的實現。哈希表把key(鍵)
經過一個固定的算法函數(此函數稱爲哈希函數/散列函數)轉換成一個整型數字,而後就將該數字對數組長度進行取餘,取餘結果就看成數組的下標,將value(值)
存儲在以該數字爲下標的數組空間裏,而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key
轉換爲對應的數組下標,並定位到該空間獲取value
。
結合下面的代碼,也許你會更容易理解:
// 哈希表
class HashTable {
constructor(){
this.table = new Array(137);
}
/** * @method hashFn 哈希函數 * @param { String } data 傳入的字符串 * @return { Number } 返回取餘的數字 */
hashFn(data){
let total = 0;
for(let i = 0; i < data.length; i++){
total += data.charCodeAt(i);
}
return total % this.table.length;
}
/** * * @param { String } data 傳入的字符串 */
put(data){
let pos = this.hashFn(data);
this.table[pos] = data;
return this;
}
// 展現
show(){
this.table && this.table.forEach((item, index) => {
if(item != undefined){
console.log(index + ' => ' + item);
}
})
}
// ...獲取值get函數等看官感興趣的話本身補充測試啦
}
let hashtable = new HashTable(),
arr = ['mouse', 'ox', 'tiger', 'rabbit', 'dragon', 'snake', 'horse', 'sheep', 'monkey', 'chicken', 'dog', 'pig'];
arr.forEach(item => {
hashtable.put(item);
});
hashtable.show();
// 5 => mouse
// 40 => dog
// 46 => pig
// 80 => rabbit
// 87 => dragon
// 94 => ox
// 111 => monkey
// 119 => snake
// 122 => sheep
// 128 => tiger
// 134 => horse
// 那麼問題來了,十二生肖裏面的_小雞_去哪裏了呢❓
// 被_小萌狗_給覆蓋了,由於其位置都是40(這個能夠本身證實下)
// 問題又來了,那麼應該如何解決這種被覆蓋的衝突呢❓
複製代碼
針對上面的問題,咱們存儲數據的時候,產生衝突的話咱們能夠像下面這樣解決:
1. 線性探測法
當發生碰撞(衝突)時,線性探測法檢查散列表中的下一個位置【有可能非順序查找位置,不必定是下一個位置】是否爲空。若是爲空,就將數據存入該位置;若是不爲空,則繼續檢查下一個位置,直到找到一個空的位置爲止。該技術是基於一個事實:每一個散列表都有不少空的單元格,可使用它們存儲數據。
2. 開鏈法
可是,當發生碰撞時,咱們任然但願將key(鍵)
存儲到經過哈希函數產生的索引位置上,那麼咱們可使用開鏈法。開鏈法是指實現哈希表底層的數組中,每一個數組元素又是一個新的數據結構,好比另外一個數組(這樣結合起來就是二位數組了),鏈表等,這樣就能存儲多個鍵了。使用這種技術,即便兩個key(鍵)
散列後的值相同,依然是被保存在一樣的位置,只不過它們是被保存在另外一個數據結構上而已。以另外一個數據結構是數組爲例,存儲的數據以下:
樹的定義:
樹(Tree):n(n >= 0)
個節點構成的有限集合。
n = 0
時,稱爲空樹;(n > 0)
,它具有如下性質:r(root)
表示;m(m > 0)
個互不相交的有限集T1,T2,...Tm
,其中每一個集合本省又是一棵樹,稱爲原來樹的子樹(SubTree)注意:
不能夠相交
;N
個節點的樹有N-1
條邊。樹的術語:
N-1
)。0
的節點(也稱葉子節點)。A
節點是B
節點的父節點,則稱B
節點是A
節點的子節點。n1
到nk
的路徑爲一個節點序列n1,n2,n3,...,nk
,ni
是ni+1
的父節點。路徑所包含邊的個數爲路徑長度。第0層
,它的子節點是第1層
,子節點的子節點是第2層
,以此類推。以下圖:
二叉樹的定義:
TL
和右子樹RT
的兩個不相交的二叉樹組成兩個
二叉樹的五種形態:
對應下圖(從左至右):
咱們接下來要講的是二叉查找樹(BST,Binary Search Tree)。二叉查找樹,也稱二叉搜索樹或二叉排序樹,是一種特殊的二叉樹,相對值較小
的值保存在左
節點中,較大
的值保存在右
節點中。二叉查找樹特殊的結構使它可以快速的進行查找、插入和刪除數據。下面咱們來實現下:
// 二叉查找樹
// 輔助節點類
class Node {
constructor(data, left, right){
this.data = data;
this.left = left;
this.right = right;
}
// 展現節點信息
show(){
return this.data;
}
}
class BST {
constructor(){
this.root = null;
}
// 插入數據
insert(data){
let n = new Node(data, null, null);
if(this.root == null){
this.root = n;
}else{
let current = this.root,
parent = null;
while(true){
parent = current;
if(data < current.data){
current = current.left;
if(current == null){
parent.left = n;
break;
}
}else{
current = current.right;
if(current == null){
parent.right = n;
break;
}
}
}
}
return this;
}
// 中序遍歷
inOrder(node){
if(!(node == null)){
this.inOrder(node.left);
console.log(node.show());
this.inOrder(node.right);
}
}
// 先序遍歷
preOrder(node){
if(!(node == null)){
console.log(node.show());
this.preOrder(node.left);
this.preOrder(node.right);
}
}
// 後序遍歷
postOrder(node){
if(!(node == null)){
this.postOrder(node.left);
this.postOrder(node.right);
console.log(node.show());
}
}
// 獲取最小值
getMin(){
let current = this.root;
while(!(current.left == null)){
current = current.left;
}
return current.data;
}
// 獲取最大值
getMax(){
let current = this.root;
while(!(current.right == null)){
current = current.right;
}
return current.data;
}
// 查找給定的值
find(data){
let current = this.root;
while(current != null){
if(current.data == data){
return current;
}else if(data < current.data){
current = current.left;
}else{
current = current.right;
}
}
return null;
}
// 移除給定的值
remove(data){
root = this.removeNode(this.root, data);
return this;
}
// 移除給定值的輔助函數
removeNode(node, data){
if(node == null){
return null;
}
if(data == node.data){
// 葉子節點
if(node.left == null && node.right == null){
return null; // 此節點置空
}
// 沒有左子樹
if(node.left == null){
return node.right;
}
// 沒有右子樹
if(node.right == null){
return node.left;
}
// 有兩個子節點的狀況
let tempNode = this.getSmallest(node.right); // 獲取右子樹
node.data = tempNode.data; // 將其右子樹的最小值賦值給刪除的那個節點值
node.right = this.removeNode(node.right, tempNode.data); // 刪除指定節點的下的最小值,也就是置其爲空
return node;
}else if(data < node.data){
node.left = this.removeNode(node.left, data);
return node;
}else{
node.right = this.removeNode(node.right, data);
return node;
}
}
// 獲取給定節點下的二叉樹最小值的輔助函數
getSmallest(node){
if(node.left == null){
return node;
}else{
return this.getSmallest(node.left);
}
}
}
let bst = new BST();
bst.insert(56).insert(22).insert(10).insert(30).insert(81).insert(77).insert(92);
bst.inOrder(bst.root); // 10, 22, 30, 56, 77, 81, 92
console.log('--中序和先序遍歷分割線--');
bst.preOrder(bst.root); // 56, 22, 10, 30, 81, 77, 92
console.log('--先序和後序遍歷分割線--');
bst.postOrder(bst.root); // 10, 30, 22, 77, 92, 81, 56
console.log('--後序遍歷和獲取最小值分割線--');
console.log(bst.getMin()); // 10
console.log(bst.getMax()); // 92
console.log(bst.find(22)); // Node { data: 22, left: Node { data: 10, left: null, right: null }, right: Node { data: 30, left: null, right: null } }
// 咱們刪除節點值爲22,而後用先序的方法遍歷,以下
console.log('--移除22的分割線--')
console.log(bst.remove(22).inOrder(bst.root)); // 10, 30, 56, 77, 81, 92
複製代碼
看了上面的代碼以後,你是否有些懵圈呢?咱們藉助幾張圖來了解下,或許你就豁然開朗了。
在遍歷的時候,咱們分爲三種遍歷方法--先序遍歷,中序遍歷和後序遍歷:
刪除節點是一個比較複雜的操做,考慮的狀況比較多:
左
子樹找節點值最大
的節點A,替換待刪除節點值,並刪除節點A右
子樹找節點值最小
的節點A,替換待刪除節點值,並刪除節點A【👆上面的示例代碼中就是這種方案】刪除兩個節點的圖解以下:
圖由邊的集合及頂點的集合組成。
咱們來了解下圖的相關術語:
0
頂點和其它兩個頂點相連,0
頂點的度就是2
v1,v2...,vn
的一個連續序列。
邊
是有
方向的。邊
是無
方向的。有權重
。無權重
。以下圖:
圖能夠用於現實中的不少系統建模,好比:
圖既然這麼方便,咱們來用代碼實現下:
// 圖
class Graph{
constructor(v){
this.vertices = v; // 頂點個數
this.edges = 0; // 邊的個數
this.adj = []; // 鄰接表或鄰接表數組
this.marked = []; // 存儲頂點是否被訪問過的標識
this.init();
}
init(){
for(let i = 0; i < this.vertices; i++){
this.adj[i] = [];
this.marked[i] = false;
}
}
// 添加邊
addEdge(v, w){
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
return this;
}
// 展現圖
showGraph(){
for(let i = 0; i < this.vertices; i++){
for(let j = 0; j < this.vertices; j++){
if(this.adj[i][j] != undefined){
console.log(i +' => ' + this.adj[i][j]);
}
}
}
}
// 深度優先搜索
dfs(v){
this.marked[v] = true;
if(this.adj[v] != undefined){
console.log("visited vertex: " + v);
}
this.adj[v].forEach(w => {
if(!this.marked[w]){
this.dfs(w);
}
})
}
// 廣度優先搜索
bfs(v){
let queue = [];
this.marked[v] = true;
queue.push(v); // 添加到隊尾
while(queue.length > 0){
let v = queue.shift(); // 從對首移除
if(v != undefined){
console.log("visited vertex: " + v);
}
this.adj[v].forEach(w => {
if(!this.marked[w]){
this.marked[w] = true;
queue.push(w);
}
})
}
}
}
let graphFirstInstance = new Graph(5);
graphFirstInstance.addEdge(0, 1).addEdge(0, 2).addEdge(1, 3).addEdge(2, 4);
graphFirstInstance.showGraph();
// 0 => 1
// 0 => 2
// 1 => 0
// 1 => 3
// 2 => 0
// 2 => 4
// 3 => 1
// 4 => 2
// ❓爲何會出現這種數據呢?它對應的圖是什麼呢?能夠思考🤔下,動手畫畫圖什麼的
console.log('--展現圖和深度優先搜索的分隔線--');
graphFirstInstance.dfs(0); // 從頂點 0 開始的深度搜索
// visited vertex: 0
// visited vertex: 1
// visited vertex: 3
// visited vertex: 2
// visited vertex: 4
console.log('--深度優先搜索和廣度優先搜索的分隔線--');
let graphSecondInstance = new Graph(5);
graphSecondInstance.addEdge(0, 1).addEdge(0, 2).addEdge(1, 3).addEdge(2, 4);
graphSecondInstance.bfs(0); // 從頂點 0 開始的廣度搜索
// visited vertex: 0
// visited vertex: 1
// visited vertex: 2
// visited vertex: 3
// visited vertex: 4
複製代碼
對於搜索圖,在上面咱們介紹了深度優先搜索 - DFS(Depth First Search)和廣度優先搜索 - BFS(Breadth First Search),結合下面的圖再回頭看下上面的代碼,你會更加容易理解這兩種搜索圖的方式。
文章中的一些案例來自coderwhy的數據結構和算法系列文章,感謝其受權
演示代碼存放地址 -- 數據結構文件夾 進入
structure
目錄能夠直接node + filename
運行
《數據結構與算法JavaScript描述》