JS高階-資料結構的封裝
最近在看了《資料結構與演算法JavaScript描述》這本書,對大學裡學的資料結構做了一次複習(其實差不多忘乾淨了,哈哈)。如果能將這些知識撿起來,融入到實際工作當中,估計編碼水平將是一次質的飛躍。帶著這個美好的願望,開始學習吧O(∩_∩)O~~
我們知道在JS中,常常用來組織資料的無非是陣列和物件(這些基礎就不介紹了)。但在資料結構中,還有一些抽象的資料型別:列表、棧、佇列、連結串列、字典、雜湊、集合、二叉樹、圖等,可以用來更好的對實際場景建模。當然這些資料型別,原生JS不支援,那麼就需要通過封裝來模擬,其底層還是陣列和物件(被看穿嘍~),接下來我們挨個來解析吧
一、列表
定義:列表是一組有序的資料
生活中經常使用到列表,通訊錄、購物車、十佳榜單等。當不需要在一個很長的序列中查詢元素或排序可以使用列表。
列表的封裝程式碼:
function List() {//列表的建構函式 this._dataStore = []; //初始化一個空陣列來儲存列表元素 this._pos = 0;//當前的位置 } List.prototype={ constructor:List, clear:function(){//清空列表 delete this._dataStore; this._dataStore = []; this._pos = 0; }, find:function(element){//在列表中查詢某一元素,若有返回位置,否則返回-1 for (var i = 0; i < this._dataStore.length; ++i) { if (this._dataStore[i] === element) {return i;} };return -1; }, contains:function(element){//判斷給定值是否在列表中 for (var i = 0; i < this._dataStore.length; ++i) { if (this._dataStore[i] === element) {return true; break;} };return false; }, insert:function(element, after){//當前位置插入新元素 var insert_pos = this.find(after); if (insert_pos > -1) {this._dataStore.splice(insert_pos+1, 0, element);return true;}; return false; }, append:function(element){this._dataStore[this._dataStore.length] = element;},//末尾新增新元素 remove:function(element){//刪除元素 var foundAt = this.find(element); if (foundAt > -1) {this._dataStore.splice(foundAt,1);return true;}; return false; }, front:function(){this._pos = 0;},//將當前位置指標設為表首 end:function(){this._pos = this._dataStore.length-1;},//將當前位置指標設為表尾 prev:function(){if (this._pos > 0) {--this._pos;}},//當前位置上移指標 next:function(){if (this._pos < this._dataStore.length-1) {++this._pos;}},//當前位置下移指標 moveTo:function(_position){this._pos = _position;},//移動當前位置指標到指定位置 length:function(){return this._dataStore.length;},//獲取列表的中元素的個數 curr_pos:function(){return this._pos;},//返回當前位置指標 getElement:function(){return this._dataStore[this._pos];},//返回當前位置的列表項 toString:function(){return this._dataStore;}//返回列表的字串形式 }
列表與陣列比較類似,只是簡單的對陣列做了二次封裝,用案例來展示一下列表的使用場景,進一步加深理解。
案例:影碟租賃自助查詢系統
var moviearr=['肖申克的救贖','教父','教父 2','低俗小說','黃金三鏢客','十二怒漢','辛德勒名單','黑暗騎士','指環王:王者歸來','搏擊俱樂部','星球大戰5:帝國反擊戰','飛越瘋人院','指環王:護戒使者','盜夢空間','好傢伙','星球大戰','七武士','黑客帝國','阿甘正傳','上帝之城']//資料 var movieList = new List();//電影列表 for (var i = 0; i < moviearr.length; ++i) {movieList.append(moviearr[i]);}//將資料新增到‘電影列表’ var customerList = new List();//客戶列表 function Customer(name, movie) {//客戶租賃物件的建構函式 this.name = name; this.movie = movie; } function checkOut(name, movie, movieList, customerList) {//某客戶需要租賃某電影,同時維護兩個列表 if(movieList.contains(movie)){//若檢索電影在列表中,新建客戶物件新增到客戶列表,同時在電影列表中刪除該電影 var c = new Customer(name, movie); customerList.append(c); movieList.remove(movie); }else{console.log(movie + " is not available.");}//若不在電影列表中,則提示不可租賃 //列印維護後的兩個列表 console.log('movieList:'+movieList.toString()+'n customerList:'+JSON.stringify(customerList.toString())) } checkOut('gavin','黑客帝國',movieList,customerList) checkOut('gavin','星球大戰',movieList,customerList) checkOut('zoe','辛德勒名單',movieList,customerList) checkOut('gulei','黑客帝國',movieList,customerList)
二、棧
定義:棧是一種特殊的列表, 棧內的元素只能通過列表的一端訪問, 這一端稱為棧頂。
棧是一種後入先出( LIFO, last-in-first-out) 的資料結構,任何不在棧頂的元素都無法訪問。 為了得到棧底的元素, 必須先拿掉上面的元素。生活中常見的例子如:餐廳的一摞盤子,只能從上面逐個取,洗淨的盤子也只能摞在最上面。
棧的封裝程式碼:
function Stack() {//棧的建構函式
this._dataStore = [];//初始化一個空陣列來儲存列表元素
this._top = 0;//記錄棧頂的位置
}
Stack.prototype={
constructor:Stack,
clear:function(){//清空棧
delete this._dataStore;
this._dataStore = []; this._top = 0;
},
push:function(element){this._dataStore[this._top++] = element;},//向棧內新增元素
pop:function(){return this._dataStore[--this._top];},//從棧內取出元素
peek:function(){return this._dataStore[this._top-1]},//檢視棧頂元素
length:function(){return this._top;}//獲取列表的中元素的個數
}
相對列表來說,棧的方法不多顯得很簡潔,同樣來幾個案例,幫助理解棧的使用場景
案例一:迴文
function isPalindrome(word){
var s = new Stack();
for (var i = 0; i < word.length; ++i) {s.push(word[i]);}
var rword = "";
while (s.length() > 0) {rword += s.pop();}
if (word == rword) {return true;}else {return false;}
}
console.log(isPalindrome("hello"));//false
console.log(isPalindrome("racecar"));//true
案例二:遞迴演示
function factorial(n) {
if (n === 0) {return 1;}else {return n * factorial(n-1);}
}
function fact(n) {
var s = new Stack();
while (n > 1) {s.push(n--);}
var product = 1;
while (s.length() > 0) {product *= s.pop();}
return product;
}
console.log(factorial(5))//120
console.log(fact(5))//120
三、佇列
定義:列隊也是一種特殊的列表, 不同的是佇列只能在隊尾插入元素, 在隊首刪除元素。
列隊是一種先進先出( First-In-First-Out, FIFO)的資料結構。排在前面的優先處理,後面的依次排隊,直到輪到它。生活中常見的例子如:列印任務池,模擬櫃檯排隊的顧客等。
佇列的封裝程式碼:
function Queue() {//佇列的建構函式
this._dataStore = [];//初始化一個空陣列來儲存元素
}
Queue.prototype={
constructor:Queue,
clear:function(){//清空佇列
delete this._dataStore;
this._dataStore = []; this._top = 0;
},
enqueue:function(element){this._dataStore.push(element)},//向隊尾新增一個元素
dequeue:function(){return this._dataStore.shift();},//刪除隊首元素
front:function(){return this._dataStore[0];},//讀取隊首元素
back:function(){return this._dataStore[this._dataStore.length-1];},//讀取隊尾元素
empty:function(){if(this._dataStore.length === 0){return true;}else{return false;}},//判斷佇列是否為空
toString:function(){//將佇列元素拼接字串
var retStr = "";
for (var i = 0; i < this._dataStore.length; ++i) {retStr += this._dataStore[i] + ",";}
return retStr;
}
}
列隊比棧稍微複雜一點,總體來說也是比較容易理解的。
案例:舞伴分配
function Dancer(name, sex) {
this.name = name;
this.sex = sex;
}
function getDancers(males, females) {
var names = ['F Allison McMillan','M Frank Opitz','M Mason McMillan','M Clayton Ruff','F Cheryl Ferenback','M Raymond Williams','F Jennifer Ingram','M Bryan Frazer','M David Durr','M Danny Martin','F Aurora Adney'];
for(var i = 0; i < names.length; ++i) {
var dancer = names[i].split(" ");
var sex = dancer[0];
var name = dancer[1];
if (sex == "F") {females.enqueue(new Dancer(name, sex));
} else {males.enqueue(new Dancer(name, sex));}
}
}
function dance(males, females) {
console.log("The dance partners are: n");
while (!females.empty() && !males.empty()) {
var person1 = females.dequeue();
var person2 = males.dequeue();
console.log("Female dancer is: " + person1.name+" and the male dancer is: " + person2.name);
}
}
var maleDancers = new Queue();
var femaleDancers = new Queue();
getDancers(maleDancers, femaleDancers);
dance(maleDancers, femaleDancers);
if (!femaleDancers.empty()) {console.log(femaleDancers.front().name + " is waiting to dance.");}
if (!maleDancers.empty()) {console.log(maleDancers.front().name + " is waiting to dance.");}
在一般情況下,從列表中刪除元素是優先刪除先入隊的元素,但有時候也可能需要使用一種優先佇列的資料來模擬,比如醫院的急診,主要通過給佇列中每個元素新增一個優先級別,並改寫dequeue方法實現。
dequeue:function() {
var priority = this._dataStore[0].code;//code表示優先級別,數值越小優先順序越高
for (var i = 1; i < this._dataStore.length; ++i) {priority =Math.min(priority,i);}
return this.dataStore.splice(priority,1);
}
四、連結串列
定義:連結串列是由一組節點組成的集合,每個節點都使用一個物件的引用指向下一個節點,這個引用叫做鏈。
除了對資料的隨機訪問,連結串列幾乎可以代替一維陣列。它與陣列的主要區別是:陣列的元素靠位置進行引用,連結串列靠相互指向進行引用。
連結串列的封裝程式碼:
function Node(element) {//連結串列中節點的建構函式
this.element = element;
this.next = null;
}
function LList() {//連結串列的建構函式
this.head = new Node("head");
}
LList.prototype={
constructor:LList,
find:function(item){//查詢連結串列,如果找到則返回該節點,否者返回頭節點
var currNode = this.head;
while (currNode.element != item) {currNode = currNode.next;}
return currNode;
},
insert:function(newElement, item){//在找到的節點後,新增一個節點
var newNode = new Node(newElement);//新增節點
var current = this.find(item);//查詢節點
newNode.next = current.next;//先將當前節點的next賦值給新節點的next
current.next = newNode;//再將當前節點的next設定為新節點
},
display:function(){
var currNode = this.head;
while (currNode.next!==null){console.log(currNode.next.element);currNode = currNode.next; }
},
findPrev:function(item){//查詢連結串列,返回當前節點的上一個節點
var currNode = this.head;
while (currNode.next!==null && currNode.next.element!==item){ currNode = currNode.next; }
return currNode;
},
remove:function(item){//在連結串列中刪除給定的節點
var prevNode = this.findPrev(item);
if (prevNode.next !== null) { prevNode.next = prevNode.next.next;}
}
}
跟之前的三種資料結構不同,連結串列沒有采用陣列作為底層資料儲存。而是採用物件節點作為基礎,同時每個節點中都含有一個next屬性指向另一個物件,與優先佇列的中的優先級別code頗為類似。總體來看連結串列是通過每個節點的next屬性,將雜湊的物件連線到了一起。
如上我們只是實現了單向連結串列,從頭遍歷到尾很簡單,想要反過來遍歷就沒那麼容易了。我們可以通過給節點增加一個prev屬性,指向它的前一個節點,也能實現雙向連結串列。當然,雙向連結串列在新增和刪除節點時的操作也要複雜一些,需要同時修改前後節點的next或prev屬性。
另外,我們還可以讓單向連結串列的尾節點指向首節點,這樣就變成了迴圈列表。這樣需要對連結串列的一些方法進行改造,防止遍歷連結串列時出現無限迴圈。
五、字典
定義:字典是一種以鍵值對形式儲存的資料結構。
JS中物件就是以字典的形式設計的,但字典的基礎是陣列,而不是物件。這樣可以進行排序,況且JS中一切皆物件,陣列也不例外。
字典的封裝程式碼:
function Dictionary() {//字典的建構函式
this._datastore = new Array();
}
Dictionary.prototype={
constructor:Dictionary,
add:function(key,value){ this._datastore[key]=value; },//增加一條鍵值對
find:function(key){ return this._datastore[key] },//查詢指定key,返回對應value的值
remove:function(key){ delete this._datastore[key] },//刪除指定key的鍵值對
showAll:function(){ //列印字典的所有鍵值對
//若需排序可以給Object.keys(this._datastore)陣列追加sort方法
Object.keys(this._datastore).forEach(function(key){console.log(key+" -> "+this._datastore[key]);}.bind(this))
},
count:function(){//返回字典所含鍵值對數量
var n = 0;
for(var key in this._datastore) {++n;}
return n;
},
clear:function(){ //清空字典
Object.keys(this._datastore).forEach(function(key){ delete this._datastore[key];}.bind(this))
}
}
字典依然採用陣列作為底層資料儲存,但是與普通按序號索引的陣列不同,它只能以key進行查詢。
六、雜湊
定義:雜湊是一種常用的資料儲存技術, 雜湊後的資料可以快速地插入或取用。 雜湊使用的資料結構叫做散列表。
是通過一個雜湊函式(Hash,雜湊)將鍵對映為一個範圍是 0 到散列表長度的數字。
雜湊的封裝程式碼:
function HashTable() {//雜湊的建構函式
this._table = new Array(137);//陣列的長度應該為質數,即預算散列表的長度
}
HashTable.prototype={
constructor:HashTable,
simpleHash:function(data){//簡單的雜湊函式(返回鍵字串的ASCII累加除陣列長度的餘數)
var total = 0;
for (var i = 0; i < data.length; ++i) {total += data.charCodeAt(i);}
return total % this._table.length;
},
betterHash:function(data){//更好的雜湊函式演算法,減少碰撞
const H = 37;
var total = 0;
for (var i = 0; i < data.length; ++i) {total += H * total + data.charCodeAt(i);}
total = total % this._table.length;
if (total < 0) {total += this._table.length-1;}
return parseInt(total);
},
put:function(data){var pos = this.simpleHash(data);this._table[pos] = data;},//使用簡單雜湊函式
//put:function(key,data){var pos = this.betterHash(key);this._table[pos] = data;},//使用高階雜湊函式
showDistro:function(){//顯示散列表中的資料
var n = 0;
for (var i = 0; i < this._table.length; ++i) {
if (this._table[i] !== undefined) {console.log(i + ": " + this._table[i]);}
}
},
get:function(key){return this._table[this.betterHash(key)];},
}
雜湊其實是通過一種機制(雜湊函式),將資料儲存到散列表對應的位置上去,當機制跟內容相關時僅出現修改才會改變。(MD5類似雜湊函式的機制)
當雜湊函式對於多個輸入產生同樣的輸出時稱為碰撞。開鏈法(用陣列儲存多個相同輸出)和探測法(線性探測下個位置,直到有空值存入)
案列:資料儲存
var students = ["David", "Jennifer", "Donnie", "Raymond", "Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
var hTable = new HashTable();
for (var i = 0; i < students.length; ++i) {hTable.put(students[i]);}
hTable.showDistro();//九條資料,被雜湊成八條,產生於了一個碰撞
七、集合
定義:是一種不含不同元素的資料結構,這些元素是無序且不重複的。
集合的封裝程式碼:
function Set() {//集合的建構函式
this._dataStore = [];
}
Set.prototype={
constructor:Set,
add:function(data){//向集合中新增元素
if (this._dataStore.indexOf(data) < 0) {this._dataStore.push(data);return true;
} else {return false;}
},
remove:function(data){//從集合中移除元素
var pos = this._dataStore.indexOf(data);
if (pos > -1) {this._dataStore.splice(pos,1);return true;
} else {return false;}
},
contains:function(){//檢查一個元素是否在集合中
if (this._dataStore.indexOf(data) > -1) {return true;} else {return false;}
},
size:function(){return this._dataStore.length},//返回集合的長度
union:function(set){//返回與另一個集合的並集
var tempSet = new Set();
for (var i = 0; i < this._dataStore.length; ++i) {tempSet.add(this._dataStore[i]);}
for (var i = 0; i < set.dataStore.length; ++i) {
if (!tempSet.contains(set.dataStore[i])) {tempSet.dataStore.push(set.dataStore[i]);}
}
return tempSet;
},
intersect:function(set){//返回與另一個集合的交集
var tempSet = new Set();
for (var i = 0; i < this._dataStore.length; ++i) {
if (set.contains(this._dataStore[i])) {tempSet.add(this._dataStore[i]);}
}
return tempSet;
},
subset:function(set){//判斷集合是否其他集合的子集
if (this.size() > set.size()) {return false;
} else {
this._dataStore.foreach(function(member){if (!set.contains(member)) {return false;}})
}
return true;
},
difference:function(set){//返回與另一個集合的補集
var tempSet = new Set();
for (var i = 0; i < this._dataStore.length; ++i) {
if (!set.contains(this._dataStore[i])) {tempSet.add(this._dataStore[i]);}
}
return tempSet;
},
show:function(){return this._dataStore;},//顯示集合中的元素
}
集合的資料結構比較簡單,主要實現了新增元素時檢查唯一性,以及交集、並集、補集的方法和子集的檢查。
八、二叉樹和二叉查詢樹
定義:樹由一組以邊連線的節點組成,二叉樹是子節點不超過兩個的特殊樹。
二叉樹的封裝程式碼:
function Node2(data, left, right) {//二叉樹中節點的建構函式
this.data = data;
this.left = left;
this.right = right;
this.show = function(){return this.data;};
}
function BST(){//二叉查詢樹的建構函式
this.root = null;
}
BST.prototype={
constructor:BST,
insert:function(data){//插入節點
var n = new Node2(data, null, null);
if (this.root == null) {
this.root = n;
} else {
var current = this.root;
var parent;
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;}
}
}
}
},
inOrder:function(node){
if (!(node == null)) {
this.inOrder(node.left);
console.log(node.show() + " ");
this.inOrder(node.right);
}
},
getMin:function(){//獲取最小的數,即最左節點
var current = this.root;
while (!(current.left == null)) {current = current.left;}
return current.data;
},
getMax:function(){//獲取最大的數,即最右節點
var current = this.root;
while (!(current.right == null)) {current = current.right;}
return current.data;
},
find:function(data){//查詢指定的值
var 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:function(data){ root = this.removeNode(this.root, data);},//呼叫removeNode刪除節點
removeNode:function(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;} // 沒有右子節點的節點
// 有兩個子節點的節點
var tempNode = getSmallest(node.right);
node.data = tempNode.data;
node.right = removeNode(node.right, tempNode.data);
return node;
} else if (data < node.data) {
node.left = removeNode(node.left, data);
return node;
} else {
node.right = removeNode(node.right, data);
return node;
}
}
}
二叉樹有點類似連結串列的資料結構,採用節點的左右屬性來指向兩個子節點。
九、圖和圖演算法
定義:圖是由邊的集合即頂點的集合組成的。常用於地圖和航班等資訊資料的建模。
圖的封裝程式碼:
function Graph(v) {//圖的建構函式,v表示頂點的數量
this.vertices = v;
this.edges = 0;
this.adj = [];
for (var i = 0; i < this.vertices; ++i) {
this.adj[i] = [];
this.adj[i].push("");
}
this.marked = [];//遍歷標誌位
for (var i = 0; i < this.vertices; ++i) {this.marked[i] = false;}
this.edgeTo = [];//路徑查詢時,儲存兩個頂點之間的邊
}
Graph.prototype={
constructor:Graph,
addEdge:function(v,w){//增加一條從頂點v到頂點w的邊
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
},
showGraph:function(){var p='';//顯示當前圖的結構
for (var i = 0; i < this.vertices; ++i) { p+='頂點'+i+' ->';
for (var j = 0; j < this.vertices; ++j) {
if (this.adj[i][j] !== undefined){ p+=this.adj[i][j]+' ';}
};p+='n';
}console.log(p)
},
dfs:function(v){//深度優先搜尋
this.marked[v] = true;
if (this.adj[v] !== undefined) {console.log("深度優先: " + v);}
for(var w in this.adj[v]) {
if(!this.marked[this.adj[v][w]]){this.dfs(this.adj[v][w]);}
}
},
bfs:function(s){//廣度優先搜尋
var queue = [];
this.marked[s] = true;
queue.push(s); // 新增到隊尾
while (queue.length > 0) {
var v = queue.shift(); // 從隊首移除
if (v!==''&&v !== undefined) {console.log("廣度優先: " + v);}
for(var w in this.adj[v]) {
if (!this.marked[this.adj[v][w]]) {
this.marked[this.adj[v][w]] = true;
this.edgeTo[this.adj[v][w]] = v;
queue.push(this.adj[v][w]);
}
}
}
},
pathTo:function(v){//獲取最短路徑,即頂點v到頂點0的邊(必須先廣度搜索生成edgeTo)
var source = 0;
if (!this.marked[v]) {return undefined;}
var path = [];
for (var i = v; i != source; i = this.edgeTo[i]) {path.push(i);}
path.push(source);
return path;
}
}
在對圖資料模型進行搜尋時,有深度優先和廣度優先兩種。當進行最短路徑查詢時,就是廣度優先搜尋的過程。