js資料結構 -- 連結串列, 雙向連結串列,迴圈連結串列
陣列作為js常用的資料結構,存取元素都非常的方便,但是其內部的實現原理跟其他計算機語言對陣列的實現都是一樣的。
由於陣列在記憶體中的儲存是連續的,所以當在陣列的開頭或者中間插入元素時,內部都需要去移動其他元素的位置,這個移動元素的成本很高。
連結串列也是儲存有序的元素集合,但不同於陣列,連結串列中的元素在記憶體中的存放並不是連續的,每個元素有一個儲存元素自身的節點和一個指向下一個元素引用(也稱為指標)的節點組成。
優點:相對於傳統的陣列,向連結串列中插入和移除元素,都不要移動其他任何元素。
缺點:要想訪問連結串列中間的一個元素,需要從 (表頭)開始迭代列表直到找到 所需的元素。
連結串列的基本骨架:
function linkedList() {
var Node = function(ele) {
this.ele = ele;
this.next = null;
}
var length = 0;
var head = null; // 用來儲存第一個元素的引用
// 對外暴露的方法
this.append = (ele) => {}; //新增元素
this.insert = (pos, ele) => {}; // 向固定位置插入元素
this.removeAt = (pos) => {}; // 移除固定位置的元素
this.remove = () => {}; // 移除結尾元素
this.indexOf = (ele) => {}; // 返回元素所在位置
this.isEmpty = () => {}; // 連結串列是否為空
this.size = () => {}; // 返回連結串列的長度
this.toString = () => {}; // 返回
this.print = () => {}; // 列印連結串列資料
}
連結串列的具體實現
向連結串列尾部追加元素
向LinkedList物件尾部新增一個元素時,可能有兩種場景:
1. 列表為空,新增的是第一個元素
2. 或者列表不為空,向其追加元素
this.append = function(element){
var node = new Node(element), //{1}
current; //{2}
if (head === null){ // 列表中的第一個節點 //{3}
head = node;
} else {
current = head; //{4}
// 迴圈列表直到找到連結串列的最後一項,
while(current.next){
current = current.next;
}
// 找到連結串列的最後一項,將其next賦為node,建立連線
current.next = node; //{5}
}
length++; // 更新連結串列的長度 //{6}
};
從連結串列中移除元素
從連結串列中移除某個元素,也可能有兩種場景:
1. 移除第一個元素
2. 移除第一個元素以外的其他元素
// 移除特定位置的元素
this.removeAt = (pos) => {
// 檢查邊界條件
if (pos > -1 && pos < length) {
let current = head; // head指向第一個元素
let previous = null; // 儲存前一個節點
let index = 0; // 計數
// 移除第一項
if (pos === 0) {
head = current.next; // 第二個元素
} else {
while (index++ < pos) {
previous = current;
current = current.next;
}
previous.next = current.next; // 將目標刪除元素的前一個元素的next指向目標刪除元素的後一個元素
}
return current.ele; // 返回要刪除節點的值
} else {
return null; // 輸入不合法
}
}
要從列表中移除當前元素,要做的就是將previous.next和current.next連線起來,當前元素就會被丟棄在計算機記憶體中,等待著被垃圾回收機制回收
在任意位置插入元素
同樣考慮兩種情況插入元素
1. 連結串列首部插入節點
2. 連結串列手部除外的其他位置插入節點
this.insert = (pos, ele) => {
// 首先檢測邊界值
if (pos >=0 && pos <= length) {
const node = new Node(ele); // 建立節點
let previous = null; // 儲存前一個節點
let current = head; // 目標位置節點
let index = 0;
// 連結串列的起點插入一個值
if (pos === 0) {
node.next = current;
head = node;
} else {
while (index++ < pos) {
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
}
length++; // 更新連結串列的長度
return true; // 插入成功
} else {
return false;
}
}
其他方法的實現
this.toString = () => {
let current = head;
let rets = '';
let index = 0;
while (current) {
rets += current.ele;
current = current.next;
}
return rets;
}
this.indexOf = (ele) => {
let current = head;
let index = -1;
while (current) {
if (current.ele === ele) {
return index++;
}
index++;
current = current.next;
}
return index;
}
this.remove = (ele) => {
var pos = this.indexOf(ele); // 返回元素所在的位置
this.removeAt(pos);
}
this.isEmpty = () => {
return length === 0;
}
this.size = () => {
return length;
}
雙向連結串列
雙向連結串列和普通連結串列的區別在於,在普通連結串列中, 一個節點只有指向下一個節點的連線,而在雙向連結串列中,連結是雙向的:一個指向下一個元素, 另一個指向前一個元素
雙向連結串列的實現
function DoublyLinkedList() {
var Node = function(ele) {
this.ele = ele;
this.prev = null; // 新增的
this.next = null;
}
var length = 0;
var head = null; // 用來儲存第一個元素的引用
var tail = null; // 用來儲存最後一個元素的引用 新增的
// 對外暴露的方法
this.append = (ele) => {}; //新增元素
this.insert = (pos, ele) => {}; // 向固定位置插入元素
this.removeAt = (pos) => {}; // 移除固定位置的元素
this.remove = () => {}; // 移除結尾元素
this.indexOf = (ele) => {}; // 返回元素所在位置
this.isEmpty = () => {}; // 連結串列是否為空
this.size = () => {}; // 返回連結串列的長度
this.toString = () => {}; // 返回
this.print = () => {}; // 列印連結串列資料
}
雙向連結串列提供了兩種迭代列表的方法:從頭到尾,或者反過來。我們也可以訪問一個特定節 的下一個或前一個元素。在單向連結串列中,如果迭代列表時錯過了要找的元素,就需要回到列表起點,重新開始迭代。這是雙向連結串列的一個優點。
插入方法實現
this.insert = (pos, ele) {
// 檢查邊界條件
if (pos > -1 && pos <= length) {
let node = new Node(ele);
let current = head; // 連結串列第一個節點
let previous = null;
let index = 0;
// 連結串列首部新增
if (pos === 0) {
if(!head) {
head = node;
tail = node;
} else {
node.next = current;
current.prev = node;
head = node;
}
} else if(pos === length) { // 最後一項
current = tail;
current.next = node;
node.pre = current;
tail = node; // 更新最後一項
} else {
// 中間位置
while (index++ < pos) {
previous = current;
current = current.next;
}
previous.next = node;
node.next = current;
current.prev = node; // 新增
node.prev = previous; // 新增
}
length++; // 更新連結串列長度
return true;
} else {
return false;
}
}
從任意位置移除元素演算法實現
需要處理三種場景:
1. 刪除連結串列第一個元素
2. 刪除連結串列最後一個元素
3. 刪除連結串列中間某個元素
this.removeAt = (pos) => {
// 檢查邊界值
if (pos > -1 && pos <= length) {
let current = head;
let previous = null;
let index = 0;
// 移除第一個元素
if (pos === 0) {
head = current.next;
if(!head){ // 只有一個元素
tail = null;
} else {
head.prev = null;
}
} else if (pos === length) {
// 移除最後一個元素
current = tail;
tail = current.prev;
tail.next = null;
} else {
// 中間位置
while (index++) {
previous = current;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
length--; // 更新連結串列長度
return true;
} else {
return false;
}
}
迴圈連結串列
迴圈連結串列可以向連結串列一樣單向引用,也可以像雙向連結串列一樣有雙向引用。迴圈連結串列於連結串列的唯一區別是:迴圈連結串列的最後一個元素指向下一個元素的指標不是null,而是指向第一個元素(head);
總結: 連結串列相比陣列最大的優點是,無需移動連結串列中的元素,就可以輕鬆實現元素的新增和刪除。因此,當需要新增和刪除很多元素時,最好的選擇是連結串列而非陣列